Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions docs/rpg/css/rpg.css
Original file line number Diff line number Diff line change
Expand Up @@ -641,13 +641,36 @@ body {
}

/* --- Title intro cinematic --- */
.title-boot-fallback {
position: relative;
width: min(100%, 560px);
text-align: center;
padding: 32px 20px;
}

.title-boot-fallback .title-intro-eyebrow,
.title-boot-fallback .title-intro-logo,
.title-boot-fallback .title-intro-tagline {
opacity: 1;
animation: none;
}

.title-boot-error,
.title-boot-note {
color: var(--magenta, #c678dd);
font-size: 8px;
line-height: 1.6;
margin: 12px 0 16px;
}

.title-intro {
position: relative;
width: min(100%, 560px);
text-align: center;
padding: 32px 20px;
opacity: 0;
transform: scale(0.96);
/* Stay readable even if the play class never lands (background tabs / rAF delay). */
opacity: 1;
transform: scale(1);
transition: opacity 0.45s ease, transform 0.45s ease;
}

Expand Down Expand Up @@ -681,17 +704,17 @@ body {
letter-spacing: 0.2em;
text-transform: uppercase;
margin: 0 0 12px;
opacity: 0;
animation: titleFadeUp 0.7s ease 0.15s forwards;
opacity: 1;
animation: titleFadeUp 0.7s ease 0.15s both;
}

.title-intro-logo {
color: var(--gold);
font-size: 16px;
line-height: 1.5;
margin: 0 0 16px;
opacity: 0;
animation: titleFadeUp 0.8s ease 0.35s forwards, titleLogoShimmer 2.4s ease 1s infinite;
opacity: 1;
animation: titleFadeUp 0.8s ease 0.35s both, titleLogoShimmer 2.4s ease 1s infinite;
}

.title-intro-rule {
Expand All @@ -706,17 +729,16 @@ body {
color: var(--magenta);
font-size: 8px;
margin: 0 0 24px;
opacity: 0;
animation: titleFadeUp 0.7s ease 1s forwards;
opacity: 1;
animation: titleFadeUp 0.7s ease 1s both;
}

.title-intro-hint {
color: var(--dim);
font-size: 8px;
margin: 0;
animation: blink 1.2s step-end infinite;
opacity: 0;
animation: titleFadeUp 0.6s ease 1.35s forwards, blink 1.2s step-end 1.8s infinite;
opacity: 1;
animation: titleFadeUp 0.6s ease 1.35s both, blink 1.2s step-end 1.8s infinite;
}

.title-intro-chips {
Expand Down Expand Up @@ -785,8 +807,9 @@ body {

/* --- Title menu panel --- */
.title-panel--enter {
opacity: 0;
transform: translateY(18px);
/* Visible by default so a missed rAF never leaves a blank title screen. */
opacity: 1;
transform: translateY(0);
transition: opacity 0.5s ease, transform 0.5s ease;
}

Expand Down
31 changes: 28 additions & 3 deletions docs/rpg/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Terminal styles come first so the RPG's pixel skin overrides them. -->
<link rel="stylesheet" href="../css/casino.css">
<link rel="stylesheet" href="css/rpg.css?v=character-sprites-2">
<link rel="stylesheet" href="css/rpg.css?v=title-boot-1">
<link rel="stylesheet" href="../css/intoxication-effects.css">
<link rel="stylesheet" href="../css/rewards-phone.css">
<script type="importmap">
Expand All @@ -39,8 +39,33 @@
<div id="rhythm-overlay" class="encounter-overlay" hidden aria-live="polite"></div>
<div id="terminal-overlay" class="encounter-overlay" hidden aria-live="polite"></div>
<div id="menu-overlay" class="encounter-overlay" hidden aria-live="polite"></div>
<div id="title-overlay" aria-live="polite"></div>
<div id="title-overlay" class="title-overlay--intro" aria-live="polite">
<div class="title-boot-fallback" id="title-boot-fallback">
<p class="title-intro-eyebrow">Welcome to</p>
<h1 class="title-intro-logo">The Mandalay Bay</h1>
<p class="title-intro-tagline">Pixel RPG · Loading…</p>
<p class="title-boot-error" id="title-boot-error" hidden></p>
</div>
</div>
</div>
<script type="module" src="js/main.js?v=character-sprites-2"></script>
<script type="module" src="js/main.js?v=title-boot-1"></script>
<script>
// Surface module-load failures that would otherwise leave a blank title.
window.addEventListener("error", (event) => {
const el = document.getElementById("title-boot-error");
const overlay = document.getElementById("title-overlay");
if (!el || !overlay || overlay.hidden) return;
if (!event.message && !event.error) return;
el.hidden = false;
el.textContent = "Could not load Pixel RPG. Refresh, or open Terminal casino mode.";
});
window.addEventListener("unhandledrejection", () => {
const el = document.getElementById("title-boot-error");
const overlay = document.getElementById("title-overlay");
if (!el || !overlay || overlay.hidden) return;
el.hidden = false;
el.textContent = "Could not load Pixel RPG. Refresh, or open Terminal casino mode.";
});
</script>
</body>
</html>
36 changes: 25 additions & 11 deletions docs/rpg/js/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Phaser from "phaser";
import { OverworldScene } from "./scenes/GameScenes.js?v=character-sprites-2";
import { TitleScreen, renderHud, renderTrainerCard } from "./scenes/TitleScreen.js";
import { OverworldScene } from "./scenes/GameScenes.js?v=title-boot-1";
import { TitleScreen, renderHud, renderTrainerCard } from "./scenes/TitleScreen.js?v=title-boot-1";
import { DialogueManager } from "./systems/DialogueManager.js";
import { SaveAdapter } from "./systems/SaveAdapter.js";
import { defaultAppearance } from "./systems/CharacterAppearance.js";
Expand Down Expand Up @@ -329,15 +329,29 @@ function showMapBanner(label, phaseLabel) {
}, 2200);
}

const title = new TitleScreen(titleRoot, (s) => {
audioManager.unlock();
audioManager.playBgm("lobby");
startOverworld(s).catch((err) => {
console.error(err);
alert(`Could not start game: ${err.message}`);
});
}, parseRpgLaunchParams());
title.show();
let title = null;
try {
if (!titleRoot) throw new Error("Missing #title-overlay — RPG shell failed to render");
title = new TitleScreen(titleRoot, (s) => {
audioManager.unlock();
audioManager.playBgm("lobby");
startOverworld(s).catch((err) => {
console.error(err);
title?.showMenu(err?.message || "Could not start the overworld.");
alert(`Could not start game: ${err.message}`);
});
}, parseRpgLaunchParams());
title.show();
} catch (err) {
console.error(err);
const bootErr = document.getElementById("title-boot-error");
if (bootErr) {
bootErr.hidden = false;
bootErr.textContent = err?.message || "Could not load Pixel RPG.";
} else {
alert(`Could not load Pixel RPG: ${err.message}`);
}
}

function parseRpgLaunchParams() {
const params = new URLSearchParams(window.location.search);
Expand Down
2 changes: 1 addition & 1 deletion docs/rpg/js/scenes/GameScenes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
setupNpcSprite,
applySpriteAppearance,
resolvePlayerSprite,
} from "../systems/TextureFactory.js?v=character-sprites-2";
} from "../systems/TextureFactory.js?v=title-boot-1";
import { normalizeAppearance } from "../systems/CharacterAppearance.js";
import {
TILE_SIZE, MAP_WIDTH, MAP_HEIGHT, buildMapLayersForId, getNpcsForMap,
Expand Down
78 changes: 59 additions & 19 deletions docs/rpg/js/scenes/TitleScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class TitleScreen {
* @param {{ launchSlotId?: number | null, launchGuest?: boolean }} [options]
*/
constructor(root, onStart, options = {}) {
if (!root) throw new Error("TitleScreen requires #title-overlay");
this.root = root;
this.onStart = onStart;
this.launchSlotId = options.launchSlotId ?? null;
Expand All @@ -45,13 +46,26 @@ export class TitleScreen {

show() {
this.root.hidden = false;
this.root.classList.remove("title-overlay--menu");
this.root.classList.remove("title-overlay--menu", "title-overlay--attract");
this.root.classList.add("title-overlay--intro");
this._renderIntro();
this._armAttract();
if (this.skipIntro) this._finishIntro();
}

/** Re-open the save library after a failed overworld boot. */
showMenu(message = null) {
this._clearIntroListeners();
this._clearAttract();
this._introDone = true;
this.root.hidden = false;
this.root.classList.remove("title-overlay--intro", "title-overlay--attract");
this.root.classList.add("title-overlay--menu");
this._renderMain();
if (message) this._showBootMessage(message);
this._armAttract();
}

hide() {
this._clearIntroListeners();
this._clearAttract();
Expand All @@ -60,6 +74,20 @@ export class TitleScreen {
this.root.classList.remove("title-overlay--intro", "title-overlay--menu", "title-overlay--attract");
}

_showBootMessage(message) {
const panel = this.root.querySelector(".title-panel");
if (!panel || !message) return;
let note = panel.querySelector(".title-boot-note");
if (!note) {
note = document.createElement("p");
note.className = "title-boot-note";
const subtitle = panel.querySelector(".subtitle");
if (subtitle) subtitle.insertAdjacentElement("afterend", note);
else panel.prepend(note);
}
note.textContent = message;
}

_armAttract() {
this._clearAttract();
this._attractTimer = setTimeout(() => this._renderAttract(), 28000);
Expand Down Expand Up @@ -153,27 +181,34 @@ export class TitleScreen {
this.root.classList.add("title-overlay--menu");

setTimeout(() => {
if (this.launchGuest) {
const guest = initSessionRpg(createGuestSession(
this.launchChips != null ? { chips: this.launchChips } : {}
));
if (this.launchArchetype) {
this._applyArchetype(guest, this.launchArchetype);
this._start(guest);
try {
if (this.launchGuest) {
const guest = createGuestSession(
this.launchChips != null ? { chips: this.launchChips } : {},
);
initSessionRpg(guest);
if (this.launchArchetype) {
this._applyArchetype(guest, this.launchArchetype);
this._start(guest);
return;
}
this._promptArchetype(guest);
return;
}
this._promptArchetype(guest);
return;
}
if (this.launchSlotId != null) {
const session = loadSlot(this.launchSlotId);
if (session) {
initSessionRpg(session);
this._start(session);
return;
if (this.launchSlotId != null) {
const session = loadSlot(this.launchSlotId);
if (session) {
initSessionRpg(session);
this._start(session);
return;
}
}
this._renderMain();
} catch (err) {
console.error(err);
this._renderMain();
this._showBootMessage(err?.message || "Could not open save library.");
}
this._renderMain();
}, intro ? 480 : 0);
}

Expand Down Expand Up @@ -241,7 +276,11 @@ export class TitleScreen {
const guestBtn = document.createElement("button");
guestBtn.type = "button";
guestBtn.textContent = "Guest visit (no save)";
guestBtn.onclick = () => this._promptArchetype(initSessionRpg(createGuestSession()));
guestBtn.onclick = () => {
const guest = createGuestSession();
initSessionRpg(guest);
this._promptArchetype(guest);
};
actions.appendChild(guestBtn);
panel.appendChild(actions);

Expand All @@ -251,6 +290,7 @@ export class TitleScreen {
panel.appendChild(link);

this.root.appendChild(panel);
panel.classList.add("title-panel--visible");
requestAnimationFrame(() => panel.classList.add("title-panel--visible"));
}

Expand Down
19 changes: 12 additions & 7 deletions docs/rpg/js/systems/CharacterSprites.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,18 @@ export function drawCharacterToCanvas(canvas, spec, dir = "down", frame = 0, pix

// Warm portrait cache for HTML overlays (creator / dialogue before Phaser boots).
function bootPortraitCache() {
const img = new Image();
img.src = assetUrl(HERO.file);
portraitImages.set(HERO.key, img);
for (const sheet of Object.values(SORA_SHEETS)) {
const soraImg = new Image();
soraImg.src = assetUrl(sheet.file);
portraitImages.set(sheet.key, soraImg);
if (typeof Image === "undefined") return;
try {
const img = new Image();
img.src = assetUrl(HERO.file);
portraitImages.set(HERO.key, img);
for (const sheet of Object.values(SORA_SHEETS)) {
const soraImg = new Image();
soraImg.src = assetUrl(sheet.file);
portraitImages.set(sheet.key, soraImg);
}
} catch (err) {
console.warn("Character portrait cache warm-up failed", err);
}
}
bootPortraitCache();
Loading