feat(risk): add portrait emotions for attacks, conquests, and army losses
- Add portraitHappy/portraitUpset helpers to react to game events - Track army snapshotted counts on attack phase entry - Trigger upset emotion when a player loses >8 armies in a turn - Show happy emotion on continent capture or defeating 5+ defender armies - Show happy on successful card trades (human and AI) - Enable opponent portrait intros in Risk - Remove deprecated setPortraitThinking method feat(paigow): widen card spread in comparison modal - Add MODAL_CARD_SPREAD constant (55px) for clearer card visibility - Use modal spread in slotX calculation for comparison display - Move Next Round button up 200px for better centering feat(opponents): add Jerry conversation audio references - Add "convo" array to Jerry's opponent entry - Reference maurice and mario conversation audio files
This commit is contained in:
parent
0be58058b4
commit
63ffa35aa4
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue