diff --git a/public/assets/speech/convo/jerry-beth.mp3 b/public/assets/speech/convo/jerry-beth.mp3 new file mode 100644 index 0000000..46ed313 Binary files /dev/null and b/public/assets/speech/convo/jerry-beth.mp3 differ diff --git a/public/assets/speech/convo/jerry-mario.mp3 b/public/assets/speech/convo/jerry-mario.mp3 new file mode 100644 index 0000000..466affd Binary files /dev/null and b/public/assets/speech/convo/jerry-mario.mp3 differ diff --git a/public/assets/speech/convo/jerry-maurice.mp3 b/public/assets/speech/convo/jerry-maurice.mp3 new file mode 100644 index 0000000..d17bc76 Binary files /dev/null and b/public/assets/speech/convo/jerry-maurice.mp3 differ diff --git a/public/data/opponents.json b/public/data/opponents.json index 53d9757..9a286fe 100644 --- a/public/data/opponents.json +++ b/public/data/opponents.json @@ -330,6 +330,10 @@ "name": "Jerry", "bio": "Well now yall know I want to play some them games!", "voice": "Life is Strange - Jed Lucan", + "convo": [ + "maurice", + "mario" + ], "speech": { "intro": [ "jerry-intro-01", diff --git a/public/src/games/paigow/PaiGowPokerGame.js b/public/src/games/paigow/PaiGowPokerGame.js index 719ed77..cd364a7 100644 --- a/public/src/games/paigow/PaiGowPokerGame.js +++ b/public/src/games/paigow/PaiGowPokerGame.js @@ -21,7 +21,8 @@ const CARD_W = 90; const CARD_H = 126; const CARD_R = 8; const CARD_SPREAD_FAN = 22; // overlap in 7-card fan at opponent seats -const CARD_SPREAD_SPLIT = 10; // tighter spread for 5-card split display +const CARD_SPREAD_SPLIT = 10; // tighter spread for 5-card split display on main table +const MODAL_CARD_SPREAD = 55; // wider spread so each card is clearly visible in comparison modal const TABLE_CY = 475; // table centre — slightly higher than Blackjack to make room for panel @@ -1269,8 +1270,7 @@ export default class PaiGowPokerGame extends Phaser.Scene { const LOW_CX = 1270; const RESULT_CENTER_Y = MODAL_TOP + Math.round(MODAL_H / 2); // 415 - // slot x for card i in a n-card spread (CARD_SPREAD_SPLIT=10) - const slotX = (cx, count, i) => cx - ((count - 1) * CARD_SPREAD_SPLIT) / 2 + i * CARD_SPREAD_SPLIT; + const slotX = (cx, count, i) => cx - ((count - 1) * MODAL_CARD_SPREAD) / 2 + i * MODAL_CARD_SPREAD; this._modal = { MODAL_X, MODAL_TOP, MODAL_W, MODAL_H, DEALER_ROW_Y, PLAYER_ROW_Y, ARROW_Y, HIGH_CX, LOW_CX, RESULT_CENTER_Y, slotX }; @@ -1706,7 +1706,7 @@ export default class PaiGowPokerGame extends Phaser.Scene { return; } - const btn = new Button(this, CX, GAME_HEIGHT / 2 + 200, 'Next Round', () => { + const btn = new Button(this, CX, GAME_HEIGHT / 2, 'Next Round', () => { btn.destroy(); this.startNewRound(); }, { width: 220, height: 56, fontSize: 24 }); diff --git a/public/src/games/risk/RiskGame.js b/public/src/games/risk/RiskGame.js index 4d508c9..8d3f2d3 100644 --- a/public/src/games/risk/RiskGame.js +++ b/public/src/games/risk/RiskGame.js @@ -69,6 +69,7 @@ export default class RiskGame extends Phaser.Scene { this.debug = false; this.gameOverShown = false; this._lastAnnouncedPhase = null; // tracks which phase was last announced for the human + this._atkSnap = null; } create() { @@ -160,7 +161,7 @@ export default class RiskGame extends Phaser.Scene { for (let seat = 0; seat < this.gs.playerCount; seat++) { let portrait = null; if (seat === this.humanSeat) portrait = createPlayerPortrait(this, PORTRAIT_CX, y, PORTRAIT_R, DEPTH.panel + 1, 'RiskGame'); - else portrait = createOpponentPortrait(this, this.opponents[seat - 1], PORTRAIT_CX, y, PORTRAIT_R, DEPTH.panel + 1, { playIntro: false }); + else portrait = createOpponentPortrait(this, this.opponents[seat - 1], PORTRAIT_CX, y, PORTRAIT_R, DEPTH.panel + 1, { playIntro: true }); // colored ring matching the player's piece color const ring = this.add.graphics().setDepth(DEPTH.panel + 2); ring.lineStyle(7, this.colorOf(seat), 1); @@ -427,8 +428,13 @@ export default class RiskGame extends Phaser.Scene { async doHumanAttack(from, to) { this.busy = true; const before = this.gs; + const defenderArmiesBefore = this.gs.armies[to]; + const defenderSeat = this.gs.owner[to]; + const ownedContsBefore = new Set(CONTINENTS.filter((c) => ownsContinent(this.gs, this.humanSeat, c.id)).map((c) => c.id)); + const defContsBefore = new Set(CONTINENTS.filter((c) => ownsContinent(this.gs, defenderSeat, c.id)).map((c) => c.id)); this.gs = resolveAttack(this.gs, from, to); await this.animateBattle(this.gs.lastBattle); + this._checkArmyLosses(); this.render(); if (this.gs.pendingConquest) { @@ -441,6 +447,11 @@ export default class RiskGame extends Phaser.Scene { if (this.gs.owner[from] === before.current && this.gs.armies[from] < 2) this.selFrom = null; this.render(); await this.animateConquest(from, to, n, this.humanSeat); + const newCont = CONTINENTS.some((c) => !ownedContsBefore.has(c.id) && ownsContinent(this.gs, this.humanSeat, c.id)); + if (defenderArmiesBefore >= 5 || newCont) this.portraitHappy(this.humanSeat); + if (defContsBefore.size > 0 && CONTINENTS.some((c) => defContsBefore.has(c.id) && !ownsContinent(this.gs, defenderSeat, c.id))) { + this.portraitUpset(defenderSeat); + } this.busy = false; this.render(); if (isGameOver(this.gs)) this.showGameOver(); @@ -542,6 +553,7 @@ export default class RiskGame extends Phaser.Scene { if (tradeBtn.disabledForTrade) return; const ids = [...selected].map((i) => cards[i].id); this.gs = tradeCards(this.gs, ids); + this.portraitHappy(seat); playSound(this, SFX.COINS); close(); if (mustTradeCards(this.gs, seat)) { this.openCardsModal(); return; } @@ -1039,6 +1051,7 @@ export default class RiskGame extends Phaser.Scene { if (this.gs.phase !== this._lastAnnouncedPhase) { this._lastAnnouncedPhase = this.gs.phase; this.announcePhase(this.gs.phase); + if (this.gs.phase === 'attack') this._initAtkSnap(); } return; } @@ -1094,7 +1107,7 @@ export default class RiskGame extends Phaser.Scene { async runAiTurn(seat) { const skill = this.gs.players[seat].skill; - this.setPortraitThinking(seat, true); + await this.delay(nextThinkDelay(skill)); // reinforce: trades then placements @@ -1103,6 +1116,7 @@ export default class RiskGame extends Phaser.Scene { const set = chooseTrade(this.gs, seat, skill); if (!set) break; this.gs = tradeCards(this.gs, set); + this.portraitHappy(seat); this.render(); await this.delay(420); } for (const step of planReinforcements(this.gs, seat, skill)) { @@ -1118,21 +1132,32 @@ export default class RiskGame extends Phaser.Scene { // attack g = 0; + this._initAtkSnap(); while (this.gs.phase === 'attack' && g++ < 400) { const atk = chooseAttack(this.gs, seat, skill); if (!atk) { this.gs = endAttack(this.gs); break; } + const defenderArmiesBefore = this.gs.armies[atk.to]; + const defenderSeat = this.gs.owner[atk.to]; + const ownedContsBefore = new Set(CONTINENTS.filter((c) => ownsContinent(this.gs, seat, c.id)).map((c) => c.id)); + const defContsBefore = new Set(CONTINENTS.filter((c) => ownsContinent(this.gs, defenderSeat, c.id)).map((c) => c.id)); this.gs = resolveAttack(this.gs, atk.from, atk.to, atk.numDice); await this.animateBattle(this.gs.lastBattle); + this._checkArmyLosses(); if (this.gs.pendingConquest) { const n = chooseAdvance(this.gs, seat, skill); const { from: cFrom, to: cTo } = this.gs.lastBattle; this.gs = advanceArmies(this.gs, n); this.render(); await this.animateConquest(cFrom, cTo, n, seat); + const newCont = CONTINENTS.some((c) => !ownedContsBefore.has(c.id) && ownsContinent(this.gs, seat, c.id)); + if (defenderArmiesBefore >= 5 || newCont) this.portraitHappy(seat); + if (defContsBefore.size > 0 && CONTINENTS.some((c) => defContsBefore.has(c.id) && !ownsContinent(this.gs, defenderSeat, c.id))) { + this.portraitUpset(defenderSeat); + } } else { this.render(); } - if (isGameOver(this.gs)) { this.setPortraitThinking(seat, false); return; } + if (isGameOver(this.gs)) return; await this.delay(200); } @@ -1142,12 +1167,34 @@ export default class RiskGame extends Phaser.Scene { this.gs = f ? fortify(this.gs, f.from, f.to, f.n) : endTurn(this.gs); this.render(); await this.delay(220); } - this.setPortraitThinking(seat, false); } - setPortraitThinking(seat, on) { - const p = this.portraits.find((x) => x.seat === seat); - p?.portrait?.playEmotion?.(on ? 'happy' : 'idle'); + portraitHappy(seat) { + this.portraits.find((x) => x.seat === seat)?.portrait?.playEmotion?.('happy'); + } + + portraitUpset(seat) { + this.portraits.find((x) => x.seat === seat)?.portrait?.playEmotion?.('upset'); + } + + _initAtkSnap() { + const snap = Array.from({ length: this.gs.playerCount }, (_, s) => + this.gs.armies.reduce((sum, n, t) => this.gs.owner[t] === s ? sum + n : sum, 0) + ); + this._atkSnap = { snap, upsetFired: new Set() }; + } + + _checkArmyLosses() { + if (!this._atkSnap) return; + const { snap, upsetFired } = this._atkSnap; + for (let s = 0; s < this.gs.playerCount; s++) { + if (upsetFired.has(s) || !this.gs.players[s].alive) continue; + const cur = this.gs.armies.reduce((sum, n, t) => this.gs.owner[t] === s ? sum + n : sum, 0); + if (snap[s] - cur > 8) { + this.portraitUpset(s); + upsetFired.add(s); + } + } } // ── game over ───────────────────────────────────────────────────────────────────