feat: add adventure battle modal and phase wheel UI
Introduce a step-by-step adventure battle modal that replaces the old fixed-delay fx when heroes walk through a dungeon. The modal shows the hero queue and dungeon layout, then plays hero-by-hero, room-by-room with battle animations, a play-by-play log, and Continue-gated progression. Key changes: - Adventure battle modal with hero/dungeon rows, battle zone, log panel, and Continue button; handles room hits, trap kills, boss wounds, and hero deaths with animations - Split adventure walk events correctly across mid-walk decision pauses using pre-mutation dungeon/hero snapshots captured in applyDecision() - New phase wheel UI (Round Start / Build / Bait / Adventure / Round End) with spinning animation and icons, driven by new phase events - Replace heroToken circles with miniHeroCard portraits at entrance gates, with shrink animation from town to gate size during heroWalks - Support proxy dragging from hover previews and deep-dive modal - Add renderInto() to Portrait.js for static portrait copies in the modal
This commit is contained in:
parent
0e93c6ed56
commit
67e4020dbc
File diff suppressed because it is too large
Load Diff
|
|
@ -1035,6 +1035,7 @@ export function baitTargets(state) {
|
|||
}
|
||||
|
||||
function runBait(state) {
|
||||
emit(state, { type: 'phaseBait' });
|
||||
const targets = baitTargets(state);
|
||||
const staying = [];
|
||||
for (const hero of state.town) {
|
||||
|
|
@ -1048,6 +1049,7 @@ function runBait(state) {
|
|||
|
||||
// ── Adventure ───────────────────────────────────────────────────────────────
|
||||
function startAdventure(state) {
|
||||
emit(state, { type: 'phaseAdventure' });
|
||||
state.phase = 'adventure';
|
||||
state.adv = { orderIdx: -1, walking: null, windowOpened: false };
|
||||
nextAdventureSeat(state);
|
||||
|
|
@ -1311,6 +1313,7 @@ function advance(state) {
|
|||
case 'roundStart':
|
||||
state.window = { id: 'buildStart', passes: [], casts: 0 };
|
||||
state.phase = 'windowBuildStart';
|
||||
emit(state, { type: 'phaseBuild' });
|
||||
continue;
|
||||
case 'windowBuildStart':
|
||||
// window handled above; when closed phase becomes 'build'
|
||||
|
|
|
|||
|
|
@ -280,7 +280,25 @@ export function createOpponentPortrait(scene, opponent, worldX, worldY, radius,
|
|||
});
|
||||
}
|
||||
|
||||
return { playEmotion, hide, show, stopVideo, fadeToEliminated, destroy, setVideoVisible };
|
||||
// Draws a fresh, independent static copy of this opponent's picture (the
|
||||
// same sprite-sheet frame the video sits in front of — never the <video>
|
||||
// itself, which always renders above canvas content regardless of depth)
|
||||
// at an arbitrary spot. Caller owns the returned GameObjects' lifecycle.
|
||||
function renderInto(x, y, radius, depth) {
|
||||
const objs = [drawBacking(scene, x, y, radius, depth)];
|
||||
if (scene.textures.exists('opponents')) {
|
||||
const maskG = scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff);
|
||||
maskG.fillCircle(x, y, radius);
|
||||
objs.push(scene.add.image(x, y, 'opponents', opponent?.spriteIndex ?? 0)
|
||||
.setDisplaySize(radius * 2, radius * 2)
|
||||
.setMask(maskG.createGeometryMask())
|
||||
.setDepth(depth + 1));
|
||||
}
|
||||
return objs;
|
||||
}
|
||||
|
||||
return { playEmotion, hide, show, stopVideo, fadeToEliminated, destroy, setVideoVisible, renderInto };
|
||||
}
|
||||
|
||||
// ── Player portrait (profile avatar with letter fallback) ─────────────────────
|
||||
|
|
@ -299,6 +317,7 @@ export function createPlayerPortrait(scene, worldX, worldY, radius, depth, scene
|
|||
|
||||
const allObjs = [backingG, placeholder];
|
||||
let destroyed = false;
|
||||
let avatarKey = null; // set once the avatar texture is loaded, for renderInto()
|
||||
|
||||
// Async avatar load
|
||||
(async () => {
|
||||
|
|
@ -327,6 +346,7 @@ export function createPlayerPortrait(scene, worldX, worldY, radius, depth, scene
|
|||
.setMask(maskG.createGeometryMask())
|
||||
.setDepth(depth + 1);
|
||||
allObjs.push(avatarImg);
|
||||
avatarKey = key;
|
||||
} catch { /* placeholder remains */ }
|
||||
})();
|
||||
|
||||
|
|
@ -348,5 +368,26 @@ export function createPlayerPortrait(scene, worldX, worldY, radius, depth, scene
|
|||
allObjs.length = 0;
|
||||
}
|
||||
|
||||
return { hide, show, stopVideo, fadeToEliminated, destroy, setVideoVisible };
|
||||
// Draws a fresh, independent static copy of the player's picture (the
|
||||
// loaded avatar if ready, else the same letter fallback) at an arbitrary
|
||||
// spot. Caller owns the returned GameObjects' lifecycle.
|
||||
function renderInto(x, y, radius, depth) {
|
||||
const objs = [drawBacking(scene, x, y, radius, depth)];
|
||||
if (avatarKey && scene.textures.exists(avatarKey)) {
|
||||
const maskG = scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff);
|
||||
maskG.fillCircle(x, y, radius);
|
||||
objs.push(scene.add.image(x, y, avatarKey)
|
||||
.setDisplaySize(radius * 2, radius * 2)
|
||||
.setMask(maskG.createGeometryMask())
|
||||
.setDepth(depth + 1));
|
||||
} else {
|
||||
objs.push(scene.add.text(x, y, initial, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: `${Math.round(radius * 0.9)}px`, color: COLORS.accentHex,
|
||||
}).setOrigin(0.5).setDepth(depth + 1));
|
||||
}
|
||||
return objs;
|
||||
}
|
||||
|
||||
return { hide, show, stopVideo, fadeToEliminated, destroy, setVideoVisible, renderInto };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue