feat(dungeonboss): scale hero card text, redesign seat card visuals, fix adventure event flow
- Scale HP and soul icon font sizes by font factor in hero cards
- Pass isHoverPreview when revealing hero cards at expanded sizes so
text scales appropriately (same logic as hover-zoom popup)
- Redesign makeSeatHeroCard to match makeHeroCard layout:
* Class-color outer border for all heroes; epic heroes get a thin
gold inner accent instead of replacing the outer border
* Top-aligned art window with class-color bar beneath it
* Center art/glyph vertically within the art window area
- Filter pendingBuild rooms from hand display to avoid duplicates with
build zone card-back placeholders
- Refactor adventure event session-end handling:
* Deduplicate resumeIdx lookup and simplify boundary logic
* Handle cascading session ends when a hero's kill/wound ends the
round, deferring remaining events to onSessionDrained callback
This commit is contained in:
parent
1753f43f2e
commit
2287b3cafe
|
|
@ -819,13 +819,13 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
// Stashed on the container so the adventure battle modal can drive a
|
// Stashed on the container so the adventure battle modal can drive a
|
||||||
// live numeric tween on it during an HP-drain animation.
|
// live numeric tween on it during an HP-drain animation.
|
||||||
cont.hpText = this.add.text(-w / 2 + 21, hy + 1, `${hero.hp}`, {
|
cont.hpText = this.add.text(-w / 2 + 21, hy + 1, `${hero.hp}`, {
|
||||||
fontFamily: 'Righteous', fontSize: '13px', color: '#fff',
|
fontFamily: 'Righteous', fontSize: `${Math.round(13 * fb)}px`, color: '#fff',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
cont.add(cont.hpText);
|
cont.add(cont.hpText);
|
||||||
const souls = heroSouls(def);
|
const souls = heroSouls(def);
|
||||||
this.drawSoulIcon(cont, w / 2 - 18, hy + 2, 9);
|
this.drawSoulIcon(cont, w / 2 - 18, hy + 2, 9);
|
||||||
cont.add(this.add.text(w / 2 - 18, hy + 2, `${souls}`, {
|
cont.add(this.add.text(w / 2 - 18, hy + 2, `${souls}`, {
|
||||||
fontFamily: 'Righteous', fontSize: '12px', color: '#2a2118',
|
fontFamily: 'Righteous', fontSize: `${Math.round(12 * fb)}px`, color: '#2a2118',
|
||||||
}).setOrigin(0.5));
|
}).setOrigin(0.5));
|
||||||
if (!opts.isHoverPreview) {
|
if (!opts.isHoverPreview) {
|
||||||
// Wide enough that the art window (300x225 native) renders at ~1:1.
|
// Wide enough that the art window (300x225 native) renders at ~1:1.
|
||||||
|
|
@ -977,7 +977,11 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
this.sfx(SFX.CARD_SHOW);
|
this.sfx(SFX.CARD_SHOW);
|
||||||
if (kind === 'room') this.makeRoomCard(0, 0, inst, fw, fh, { parent: cont, showText: true, hover: false, noHoverPreview: true });
|
if (kind === 'room') this.makeRoomCard(0, 0, inst, fw, fh, { parent: cont, showText: true, hover: false, noHoverPreview: true });
|
||||||
else if (kind === 'spell') this.makeSpellCard(0, 0, inst, fw, fh, { parent: cont, hover: false, noHoverPreview: true });
|
else if (kind === 'spell') this.makeSpellCard(0, 0, inst, fw, fh, { parent: cont, hover: false, noHoverPreview: true });
|
||||||
else this.makeHeroCard(0, 0, inst, fw, fh, { parent: cont, hover: false, noHoverPreview: true });
|
// isHoverPreview bumps makeHeroCard's otherwise-fixed-size text
|
||||||
|
// (name/class/HP/soul labels) — needed here since this reveal is
|
||||||
|
// shown at ~3x a normal town card's width (DRAW_FULL_W.hero vs
|
||||||
|
// TOWN_HERO_W), same reasoning as the existing hover-zoom popup.
|
||||||
|
else this.makeHeroCard(0, 0, inst, fw, fh, { parent: cont, hover: false, noHoverPreview: true, isHoverPreview: true });
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: cont, scaleX: 1, duration: 110, ease: 'Sine.easeOut',
|
targets: cont, scaleX: 1, duration: 110, ease: 'Sine.easeOut',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
|
|
@ -1503,17 +1507,28 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
const cont = this.add.container(x, y);
|
const cont = this.add.container(x, y);
|
||||||
const g = this.add.graphics();
|
const g = this.add.graphics();
|
||||||
g.fillStyle(C.cardBg, 1); g.fillRoundedRect(-w / 2, -h / 2, w, h, 4);
|
g.fillStyle(C.cardBg, 1); g.fillRoundedRect(-w / 2, -h / 2, w, h, 4);
|
||||||
g.lineStyle(def.epic ? 3 : 2, def.epic ? C.gold : info.color, 1);
|
// Outer border is always the class color (matches makeHeroCard) — epic
|
||||||
|
// status gets its own thin inner accent instead of overriding this, so an
|
||||||
|
// epic hero's class still reads at a glance.
|
||||||
|
g.lineStyle(2, info.color, 1);
|
||||||
g.strokeRoundedRect(-w / 2, -h / 2, w, h, 4);
|
g.strokeRoundedRect(-w / 2, -h / 2, w, h, 4);
|
||||||
|
if (def.epic) { g.lineStyle(1.5, C.gold, 1); g.strokeRoundedRect(-w / 2 + 2.5, -h / 2 + 2.5, w - 5, h - 5, 3); }
|
||||||
|
// Top-aligned art window + class-color bar beneath it, echoing
|
||||||
|
// makeHeroCard's composition instead of a near-full-bleed centered image.
|
||||||
|
const artH = h * 0.58;
|
||||||
|
g.fillStyle(C.heroWindow, 1); g.fillRect(-w / 2 + 3, -h / 2 + 3, w - 6, artH);
|
||||||
|
const barH = Math.max(4, h * 0.09);
|
||||||
|
g.fillStyle(info.color, 1); g.fillRoundedRect(-w / 2 + 3, -h / 2 + 3 + artH + 2, w - 6, barH, 2);
|
||||||
cont.add(g);
|
cont.add(g);
|
||||||
const art = this.artFor('hero', hero.id);
|
const art = this.artFor('hero', hero.id);
|
||||||
|
const artCY = -h / 2 + 3 + artH / 2;
|
||||||
if (art) {
|
if (art) {
|
||||||
const img = this.add.image(0, -2, art.key, art.frame);
|
const img = this.add.image(0, artCY, art.key, art.frame);
|
||||||
img.setScale(Math.min((w - 6) / Math.max(img.width, 1), (h - 6) / Math.max(img.height, 1)));
|
img.setScale(Math.min((w - 6) / Math.max(img.width, 1), artH / Math.max(img.height, 1)));
|
||||||
cont.add(img);
|
cont.add(img);
|
||||||
} else {
|
} else {
|
||||||
const glyph = def.fool ? '🃏' : def.cls === 'fighter' ? '🛡' : def.cls === 'mage' ? '🔮' : def.cls === 'thief' ? '🗝' : '📿';
|
const glyph = def.fool ? '🃏' : def.cls === 'fighter' ? '🛡' : def.cls === 'mage' ? '🔮' : def.cls === 'thief' ? '🗝' : '📿';
|
||||||
cont.add(this.add.text(0, -2, glyph, { fontSize: `${Math.round(h * 0.5)}px` }).setOrigin(0.5).setAlpha(0.85));
|
cont.add(this.add.text(0, artCY, glyph, { fontSize: `${Math.round(artH * 0.55)}px` }).setOrigin(0.5).setAlpha(0.85));
|
||||||
}
|
}
|
||||||
const by = h / 2 + 12;
|
const by = h / 2 + 12;
|
||||||
const bb = this.add.graphics();
|
const bb = this.add.graphics();
|
||||||
|
|
@ -1594,7 +1609,13 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
const zoneActive = mode.type === 'discard' || mode.type === 'setupDiscard';
|
const zoneActive = mode.type === 'discard' || mode.type === 'setupDiscard';
|
||||||
const buildActive = mode.type === 'build';
|
const buildActive = mode.type === 'build';
|
||||||
const inZone = (uid) => zoneActive && (mode.selected || []).includes(uid);
|
const inZone = (uid) => zoneActive && (mode.selected || []).includes(uid);
|
||||||
const rooms = p.hand.rooms.filter((c) => !inZone(c.uid));
|
// A room already committed to this round's secret build (p.pendingBuild
|
||||||
|
// stays set — not cleared until flipBuilds — even after the human's own
|
||||||
|
// decision is submitted, while other seats are still deciding) already
|
||||||
|
// has its own card-back placeholder in the build zone (see the
|
||||||
|
// p.pendingBuild block in renderHumanBoard) — don't also show it in hand.
|
||||||
|
const pendingBuildUid = p.pendingBuild?.roomUid;
|
||||||
|
const rooms = p.hand.rooms.filter((c) => !inZone(c.uid) && c.uid !== pendingBuildUid);
|
||||||
const spells = p.hand.spells.filter((c) => !inZone(c.uid));
|
const spells = p.hand.spells.filter((c) => !inZone(c.uid));
|
||||||
const total = rooms.length + spells.length;
|
const total = rooms.length + spells.length;
|
||||||
if (!total) return;
|
if (!total) return;
|
||||||
|
|
@ -3096,15 +3117,17 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const events = takeEvents(this.gs);
|
const events = takeEvents(this.gs);
|
||||||
|
const resumeIdx = events.findIndex((e) => ADV_RESUME_MARKERS.includes(e.type));
|
||||||
const endIdx = events.findIndex((e) => ADV_SESSION_END_MARKERS.includes(e.type));
|
const endIdx = events.findIndex((e) => ADV_SESSION_END_MARKERS.includes(e.type));
|
||||||
if (endIdx !== -1) {
|
if (endIdx !== -1 && (resumeIdx === -1 || endIdx < resumeIdx)) {
|
||||||
// See ADV_SESSION_END_MARKERS — everything from here on belongs to a
|
// See ADV_SESSION_END_MARKERS — the round/game ends here with nothing
|
||||||
// new round/phase (or the game-over screen), not this seat's walk.
|
// (further) for this seat's walk to react to. Everything from here on
|
||||||
// Close this modal first, then hand the rest to the normal pipeline —
|
// belongs to a new round/phase (or the game-over screen), not this
|
||||||
// exactly like onSessionDrained does for the ordinary drain path, just
|
// seat's walk — close this modal first, then hand the rest to the
|
||||||
// using the fresh tail of THIS takeEvents() call rather than whatever
|
// normal pipeline, exactly like onSessionDrained does for the ordinary
|
||||||
// (by now almost certainly empty) batch this session originally opened
|
// drain path, just using the fresh tail of THIS takeEvents() call
|
||||||
// with.
|
// rather than whatever (by now almost certainly empty) batch this
|
||||||
|
// session originally opened with.
|
||||||
const narration = events.slice(0, endIdx);
|
const narration = events.slice(0, endIdx);
|
||||||
const rest = events.slice(endIdx);
|
const rest = events.slice(endIdx);
|
||||||
this.playAdventureNarration(narration, () => {
|
this.playAdventureNarration(narration, () => {
|
||||||
|
|
@ -3112,9 +3135,9 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const boundary = events.findIndex((e) => ADV_RESUME_MARKERS.includes(e.type));
|
const boundary = resumeIdx;
|
||||||
const narration = boundary === -1 ? events : events.slice(0, boundary);
|
const narration = boundary === -1 ? events : events.slice(0, boundary);
|
||||||
const resumed = boundary === -1 ? [] : events.slice(boundary);
|
let resumed = boundary === -1 ? [] : events.slice(boundary);
|
||||||
// Unlike runAdventureWalk's two call sites, `resumed` never passes through
|
// Unlike runAdventureWalk's two call sites, `resumed` never passes through
|
||||||
// splitAdventureWalk, so any hero who first enters AFTER this window's
|
// splitAdventureWalk, so any hero who first enters AFTER this window's
|
||||||
// pause (i.e. its heroEnters lands here rather than in the initial batch)
|
// pause (i.e. its heroEnters lands here rather than in the initial batch)
|
||||||
|
|
@ -3123,6 +3146,22 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
// no-op window auto-closes straight through to the next seat's own walk
|
// no-op window auto-closes straight through to the next seat's own walk
|
||||||
// in the same takeEvents() drain.
|
// in the same takeEvents() drain.
|
||||||
if (resumed.length) this.syncHeroRow(resumed.filter((e) => e.seat === ab.seat));
|
if (resumed.length) this.syncHeroRow(resumed.filter((e) => e.seat === ab.seat));
|
||||||
|
// The walk resuming here can ITSELF cascade straight into round/game end
|
||||||
|
// (e.g. this hero's own kill/wound — the thing ADV_RESUME_MARKERS just
|
||||||
|
// found — was the last thing left to happen this round, and nobody could
|
||||||
|
// react to it either). Keep that hero's own kill/wound in `resumed` so
|
||||||
|
// the rich per-step pipeline still plays it (and its banner) normally,
|
||||||
|
// but cut off and defer anything from the session-end marker onward —
|
||||||
|
// rebinding onSessionDrained to the fresh tail, since the stale one
|
||||||
|
// captured at session-open time won't contain it — rather than let
|
||||||
|
// nextAdventureStep silently skip past unplayed round/heroRevealed events
|
||||||
|
// it doesn't recognize.
|
||||||
|
const innerEndIdx = resumed.findIndex((e) => ADV_SESSION_END_MARKERS.includes(e.type));
|
||||||
|
if (innerEndIdx !== -1) {
|
||||||
|
const sessionRest = resumed.slice(innerEndIdx);
|
||||||
|
resumed = resumed.slice(0, innerEndIdx);
|
||||||
|
ab.onSessionDrained = () => this.closeAdventureModal(() => { this.busy = false; this.playEvents(sessionRest); });
|
||||||
|
}
|
||||||
this.playAdventureNarration(narration, () => {
|
this.playAdventureNarration(narration, () => {
|
||||||
ab.events.push(...resumed);
|
ab.events.push(...resumed);
|
||||||
this.advanceAdventureStep();
|
this.advanceAdventureStep();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue