diff --git a/src/games/balatro/BalatroGame.js b/src/games/balatro/BalatroGame.js index 579bf48..73d53e7 100644 --- a/src/games/balatro/BalatroGame.js +++ b/src/games/balatro/BalatroGame.js @@ -369,7 +369,10 @@ export default class BalatroGame extends Phaser.Scene { const debuffed = opts.debuffed; if (card.faceDown && !opts.reveal) { - if (this.textures.exists('cardbacks')) { + const af = this.run ? this.artFrame('deckBackSheet', 'deckBacks', this.run.deckId) : null; + if (af) { + this.addRoundedArt(cont, af.key, af.frame, w, h, 10); + } else if (this.textures.exists('cardbacks')) { const img = this.add.image(0, 0, 'cardbacks', 0).setDisplaySize(w, h); cont.add(img); } else { @@ -714,15 +717,24 @@ export default class BalatroGame extends Phaser.Scene { // Blind badge const blind = BLINDS[run.blind]; const boss = run.blind === 'boss' ? bossFor(run) : null; + const bossIcon = boss ? this.artFrame('bossIconSheet', 'bosses', boss.id) : null; const badgeColor = run.blind === 'boss' ? 0xc22f3a : run.blind === 'big' ? 0x8556d6 : 0x2f6fce; - this.panel(X + 16, 40, W - 32, boss ? 168 : 128, { fill: badgeColor, alpha: 0.35, strokeColor: badgeColor }); - this.text(X + W / 2, 58, boss ? boss.name : blind.name, 30, C.ink, { ox: 0.5, bold: true }); - this.text(X + W / 2, 100, `Score at least`, 17, C.muted, { ox: 0.5 }); - this.text(X + W / 2, 124, fmtChips(run.blindChips), 32, '#ff8a6a', { ox: 0.5, bold: true }); - if (boss) this.text(X + W / 2, 166, boss.desc, 16, '#ffb9a8', { ox: 0.5, align: 'center', wrap: W - 60 }); + const badgeH = boss ? (bossIcon ? 268 : 168) : 128; + this.panel(X + 16, 40, W - 32, badgeH, { fill: badgeColor, alpha: 0.35, strokeColor: badgeColor }); + let ny = 58; + if (bossIcon) { + const iconCont = this.add.container(X + W / 2, 40 + 16 + 28); + this.addRoundedArt(iconCont, bossIcon.key, bossIcon.frame, 56, 56, 8); + this.add2(iconCont); + ny = 134; + } + this.text(X + W / 2, ny, boss ? boss.name : blind.name, 30, C.ink, { ox: 0.5, bold: true }); + this.text(X + W / 2, ny + 42, `Score at least`, 17, C.muted, { ox: 0.5 }); + this.text(X + W / 2, ny + 66, fmtChips(run.blindChips), 32, '#ff8a6a', { ox: 0.5, bold: true }); + if (boss) this.text(X + W / 2, ny + 108, boss.desc, 16, '#ffb9a8', { ox: 0.5, align: 'center', wrap: W - 60 }); // Round score - const sy = boss ? 226 : 190; + const sy = boss ? (bossIcon ? 326 : 226) : 190; this.text(X + 24, sy, 'Round score', 18, C.muted); this._roundScoreText = this.text(X + W - 24, sy - 6, fmtChips(this._roundScoreOverride ?? run.roundScore), 34, C.ink, { ox: 1, bold: true }); @@ -935,10 +947,17 @@ export default class BalatroGame extends Phaser.Scene { const run = this.run; const x = 1840, y = 880, w = 110, h = 148; const deck = DECK_BY_ID[run.deckId]; - const g = this.add.graphics(); - g.fillStyle(deck.color, 0.9); g.fillRoundedRect(x - w / 2, y - h / 2, w, h, 8); - g.lineStyle(2, 0xffffff, 0.4); g.strokeRoundedRect(x - w / 2, y - h / 2, w, h, 8); - this.add2(g); + const af = this.artFrame('deckBackSheet', 'deckBacks', deck.id); + if (af) { + const cont = this.add.container(x, y); + this.addRoundedArt(cont, af.key, af.frame, w, h, 8); + this.add2(cont); + } else { + const g = this.add.graphics(); + g.fillStyle(deck.color, 0.9); g.fillRoundedRect(x - w / 2, y - h / 2, w, h, 8); + g.lineStyle(2, 0xffffff, 0.4); g.strokeRoundedRect(x - w / 2, y - h / 2, w, h, 8); + this.add2(g); + } this.text(x, y + h / 2 + 14, `${run.drawPile.length}/${run.deck.length}`, 18, C.ink, { ox: 0.5 }); } diff --git a/src/games/balatro/BalatroViews.js b/src/games/balatro/BalatroViews.js index 199aaf0..7b1555c 100644 --- a/src/games/balatro/BalatroViews.js +++ b/src/games/balatro/BalatroViews.js @@ -119,17 +119,26 @@ export function renderBlindSelect(scene) { const isCur = i === curIdx; const done = i < curIdx; const boss = stage === 'boss' ? bossFor(run) : null; + const bossIcon = boss ? scene.artFrame('bossIconSheet', 'bosses', boss.id) : null; const color = stage === 'boss' ? 0xc22f3a : stage === 'big' ? 0x8556d6 : 0x2f6fce; scene.panel(x - w / 2, y - h / 2, w, h, { fill: isCur ? color : C.panel, alpha: isCur ? 0.4 : 0.75, strokeColor: color }); - scene.text(x, y - h / 2 + 46, boss ? boss.name : BLINDS[stage].name, 32, C.ink, { ox: 0.5, bold: true }); + const top = y - h / 2; + let ny = top + 46; + if (bossIcon) { + const iconCont = scene.add.container(x, top + 44); + scene.addRoundedArt(iconCont, bossIcon.key, bossIcon.frame, 56, 56, 8); + scene.add2(iconCont); + ny = top + 94; + } + scene.text(x, ny, boss ? boss.name : BLINDS[stage].name, 32, C.ink, { ox: 0.5, bold: true }); // Requirement uses the boss multiplier for the boss column. const req = stage === 'boss' ? Math.floor(anteBase(run.ante) * ((boss && boss.reqMult) || 2)) : Math.floor(anteBase(run.ante) * BLINDS[stage].mult); - scene.text(x, y - h / 2 + 110, 'Score at least', 18, C.muted, { ox: 0.5 }); - scene.text(x, y - h / 2 + 142, fmtChips(req), 36, '#ff8a6a', { ox: 0.5, bold: true }); - scene.text(x, y - h / 2 + 200, `Reward: $${BLINDS[stage].reward}`, 22, C.money, { ox: 0.5 }); - if (boss) scene.text(x, y - h / 2 + 260, boss.desc, 20, '#ffb9a8', { ox: 0.5, align: 'center', wrap: w - 60 }); + scene.text(x, ny + 64, 'Score at least', 18, C.muted, { ox: 0.5 }); + scene.text(x, ny + 96, fmtChips(req), 36, '#ff8a6a', { ox: 0.5, bold: true }); + scene.text(x, ny + 154, `Reward: $${BLINDS[stage].reward}`, 22, C.money, { ox: 0.5 }); + if (boss) scene.text(x, ny + 214, boss.desc, 20, '#ffb9a8', { ox: 0.5, align: 'center', wrap: w - 60 }); if (done) { scene.text(x, y + h / 2 - 120, '✔ cleared', 26, '#7ce07c', { ox: 0.5 }); diff --git a/src/games/swdbg/SWDBGGame.js b/src/games/swdbg/SWDBGGame.js index 440e6f9..efa368b 100644 --- a/src/games/swdbg/SWDBGGame.js +++ b/src/games/swdbg/SWDBGGame.js @@ -97,7 +97,7 @@ const CARD_DEEPDIVE_ZONES = [ }, { id: 'bounty', side: 'right', title: 'Bounty', - text: 'Commit enough squad attack (matching this number) to defeat this row card instead of buying it, and claim the reward shown.', + text: (def) => `Commit ${def.target} or more squad attack to defeat this row card instead of buying it. Reward: ${bountyRewardSentence(def.reward)}.`, condition: (def) => def.target != null, rect: (w) => ({ x: -w / 2 + 10, y: 208, w: w - 20, h: 30 }), anchorX: (w) => w / 2 + 130, @@ -105,6 +105,32 @@ const CARD_DEEPDIVE_ZONES = [ ]; const DEEPDIVE_ZONES = { card: CARD_DEEPDIVE_ZONES }; +// Bounty reward → short display bits, shared by the card footer and the +// bounty deep-dive tooltip so both stay in sync as reward shapes are added. +function bountyRewardBits(reward) { + const r = reward || {}; + const bits = []; + if (r.resources) bits.push(`${r.resources}▣`); + if (r.force) bits.push(`${r.force}◈`); + if (r.draw) bits.push(`draw ${r.draw}`); + if (r.exile) bits.push(`exile ${r.exile}`); + if (r.freePurchase) bits.push('free buy'); + return bits; +} + +// Bounty reward → a plain-language sentence fragment for the deep-dive +// tooltip, spelling out exactly what this card's bounty grants. +function bountyRewardSentence(reward) { + const r = reward || {}; + const bits = []; + if (r.resources) bits.push(`${r.resources} ▣ Resources`); + if (r.force) bits.push(`${r.force} ◈ Force`); + if (r.draw) bits.push(`draw ${r.draw} card${r.draw > 1 ? 's' : ''}`); + if (r.exile) bits.push(`exile ${r.exile} card${r.exile > 1 ? 's' : ''} from your hand or discard`); + if (r.freePurchase) bits.push('a free purchase from the galaxy row'); + return bits.length ? bits.join(', ') : 'the reward shown'; +} + // Mirrors makeCard's own stat() helper: slots are only consumed by truthy // stats, in the fixed order attack -> resources -> force. function statSlotY(def, key) { @@ -196,7 +222,9 @@ export default class SWDBGGame extends Phaser.Scene { fontFamily: 'Righteous', fontSize: '23px', color: C.goldHex, backgroundColor: '#05070fdd', padding: { x: 16, y: 6 }, }).setOrigin(0.5).setDepth(DEPTH.ui); - if (this.opponents[0]) createOpponentPortrait(this, this.opponents[0], 140, 128, 44, DEPTH.ui); + if (this.opponents[0]) { + this._portrait = createOpponentPortrait(this, this.opponents[0], 140, 128, 44, DEPTH.ui); + } createPlayerPortrait(this, 140, 812, 44, DEPTH.ui, 'SWDBGGame'); } @@ -337,11 +365,7 @@ export default class SWDBGGame extends Phaser.Scene { } // bounty footer if (def.target != null) { - const r = def.reward || {}; - const bits = []; - if (r.resources) bits.push(`${r.resources}▣`); - if (r.force) bits.push(`${r.force}◈`); - if (r.draw) bits.push(`draw ${r.draw}`); + const bits = bountyRewardBits(def.reward); cont.add(this.add.text(0, h / 2 - 12, `◎ ${def.target} → ${bits.join(' ')}`, { fontFamily: '"Julius Sans One"', fontSize: `${Math.max(9, Math.round(w / 14.5))}px`, color: C.bountyHex, }).setOrigin(0.5)); @@ -2267,6 +2291,10 @@ export default class SWDBGGame extends Phaser.Scene { ? `💥 ${name} HAS FALLEN (${e.lost}/${this.gs.meta.basesToWin})` : `💥 YOU DESTROYED ${name.toUpperCase()} (${e.lost}/${this.gs.meta.basesToWin})`); this.cameras.main.shake(240, 0.006); + // Opponent reaction: happy when they destroy a base, upset when theirs falls + if (this._portrait) { + this._portrait.playEmotion(e.seat === this.humanSeat ? 'happy' : 'upset'); + } break; } case 'baseChosen':