feat(risk): add rich combat animations and enhanced move modal

- Add animated attack arrows with quadratic bezier curves from attacker to defender
- Implement battle modal that zooms from battlefield to center, shows animated dice rolling, and resolves pair-by-pair with colored arrows
- Add conquest animation with straight-line arrow and floating +/- army count text
- Store attacker/defender seat info in lastBattle for proper color theming
- Add Min/Max quick-select buttons to army deployment modal
- Ensure busy state is held during conquest animations to prevent input conflicts
This commit is contained in:
Brian Fertig 2026-06-18 19:40:19 -06:00
parent 036947885d
commit cb506696a6
2 changed files with 376 additions and 25 deletions

View File

@ -396,11 +396,15 @@ export default class RiskGame extends Phaser.Scene {
if (this.gs.pendingConquest) { if (this.gs.pendingConquest) {
const pc = this.gs.pendingConquest; const pc = this.gs.pendingConquest;
this.busy = false; this.busy = false;
this.openMoveModal(pc.minMove, pc.maxMove, pc.minMove, (n) => { this.openMoveModal(pc.minMove, pc.maxMove, pc.minMove, async (n) => {
this.busy = true;
this.gs = advanceArmies(this.gs, n); this.gs = advanceArmies(this.gs, n);
// keep attacking from same territory if still able // keep attacking from same territory if still able
if (this.gs.owner[from] === before.current && this.gs.armies[from] < 2) this.selFrom = null; if (this.gs.owner[from] === before.current && this.gs.armies[from] < 2) this.selFrom = null;
this.render(); this.render();
await this.animateConquest(from, to, n, this.humanSeat);
this.busy = false;
this.render();
if (isGameOver(this.gs)) this.showGameOver(); if (isGameOver(this.gs)) this.showGameOver();
}); });
return; return;
@ -500,7 +504,7 @@ export default class RiskGame extends Phaser.Scene {
let val = Math.max(min, Math.min(initial, max)); let val = Math.max(min, Math.min(initial, max));
const objs = []; const objs = [];
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2; const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
const W = 460, H = 240; const W = 640, H = 240;
const overlay = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55).setInteractive().setDepth(DEPTH.popup); objs.push(overlay); const overlay = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55).setInteractive().setDepth(DEPTH.popup); objs.push(overlay);
const panel = this.add.graphics().setDepth(DEPTH.popup + 1); objs.push(panel); const panel = this.add.graphics().setDepth(DEPTH.popup + 1); objs.push(panel);
panel.fillStyle(COLORS.panel, 1); panel.fillRoundedRect(cx - W / 2, cy - H / 2, W, H, 14); panel.fillStyle(COLORS.panel, 1); panel.fillRoundedRect(cx - W / 2, cy - H / 2, W, H, 14);
@ -515,34 +519,374 @@ export default class RiskGame extends Phaser.Scene {
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.popup + 2); objs.push(rng); }).setOrigin(0.5).setDepth(DEPTH.popup + 2); objs.push(rng);
const upd = () => valTxt.setText(String(val)); const upd = () => valTxt.setText(String(val));
const minus = new Button(this, cx - 150, cy - 4, '', () => { val = Math.max(min, val - 1); upd(); }, { width: 64, height: 64, fontSize: 34 }).setDepth(DEPTH.popup + 2); objs.push(minus); const btnMin = new Button(this, cx - 230, cy - 4, 'Min', () => { val = min; upd(); }, { width: 80, height: 64, fontSize: 20, variant: 'ghost' }).setDepth(DEPTH.popup + 2); objs.push(btnMin);
const plus = new Button(this, cx + 150, cy - 4, '+', () => { val = Math.min(max, val + 1); upd(); }, { width: 64, height: 64, fontSize: 34 }).setDepth(DEPTH.popup + 2); objs.push(plus); const minus = new Button(this, cx - 130, cy - 4, '', () => { val = Math.max(min, val - 1); upd(); }, { width: 64, height: 64, fontSize: 34 }).setDepth(DEPTH.popup + 2); objs.push(minus);
const plus = new Button(this, cx + 130, cy - 4, '+', () => { val = Math.min(max, val + 1); upd(); }, { width: 64, height: 64, fontSize: 34 }).setDepth(DEPTH.popup + 2); objs.push(plus);
const btnMax = new Button(this, cx + 230, cy - 4, 'Max', () => { val = max; upd(); }, { width: 80, height: 64, fontSize: 20, variant: 'ghost' }).setDepth(DEPTH.popup + 2); objs.push(btnMax);
const ok = new Button(this, cx, cy + H / 2 - 34, 'Confirm', () => { objs.forEach((o) => o.destroy()); onConfirm(val); }, { width: 260, height: 46, fontSize: 22 }).setDepth(DEPTH.popup + 2); objs.push(ok); const ok = new Button(this, cx, cy + H / 2 - 34, 'Confirm', () => { objs.forEach((o) => o.destroy()); onConfirm(val); }, { width: 260, height: 46, fontSize: 22 }).setDepth(DEPTH.popup + 2); objs.push(ok);
} }
// ── combat animation ──────────────────────────────────────────────────────────── // ── combat animation ────────────────────────────────────────────────────────────
async animateBattle(b) { async animateAttackArrow(b) {
if (!b) return; const seat = this.gs.owner[b.from];
playSound(this, SFX.DICE_ROLL); const color = this.colorOf(seat);
const from = this.boardToScreen(TERRITORIES[b.from].x, TERRITORIES[b.from].y); const from = this.boardToScreen(TERRITORIES[b.from].x, TERRITORIES[b.from].y);
const to = this.boardToScreen(TERRITORIES[b.to].x, TERRITORIES[b.to].y); const to = this.boardToScreen(TERRITORIES[b.to].x, TERRITORIES[b.to].y);
const mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2;
const objs = []; // Quadratic bezier control point arching upward (negative-Y) from the midpoint
const drawDie = (x, y, val, col) => { const dx = to.x - from.x, dy = to.y - from.y;
const g = this.add.graphics().setDepth(DEPTH.dice); const len = Math.sqrt(dx * dx + dy * dy) || 1;
g.fillStyle(col, 1); g.fillRoundedRect(x - 18, y - 18, 36, 36, 6); let perpX = -dy / len, perpY = dx / len;
g.lineStyle(2, 0x000000, 0.6); g.strokeRoundedRect(x - 18, y - 18, 36, 36, 6); if (perpY > 0) { perpX = -perpX; perpY = -perpY; } // always arch toward top of screen
const t = this.add.text(x, y, String(val), { fontFamily: 'Righteous', fontSize: '22px', color: col === 0xffffff ? '#222' : '#fff' }).setOrigin(0.5).setDepth(DEPTH.dice + 1); const archH = Math.min(len * 0.35, 180);
objs.push(g, t); const cpx = (from.x + to.x) / 2 + perpX * archH;
const cpy = (from.y + to.y) / 2 + perpY * archH;
const g = this.add.graphics().setDepth(DEPTH.badge - 1);
const LINE_W = 6, STROKE_W = 3, ARROW_LEN = 26, ARROW_WID = 12;
const drawArc = (t) => {
g.clear();
const steps = Math.max(2, Math.ceil(t * 64));
// compute bezier points once, reuse for both passes
const pts = [];
for (let i = 0; i <= steps; i++) {
const tt = (i / steps) * t;
pts.push(
(1 - tt) * (1 - tt) * from.x + 2 * (1 - tt) * tt * cpx + tt * tt * to.x,
(1 - tt) * (1 - tt) * from.y + 2 * (1 - tt) * tt * cpy + tt * tt * to.y,
);
}
const drawPath = (w, col, alpha) => {
g.lineStyle(w, col, alpha);
g.beginPath();
for (let i = 0; i <= steps; i++) {
if (i === 0) g.moveTo(pts[i * 2], pts[i * 2 + 1]);
else g.lineTo(pts[i * 2], pts[i * 2 + 1]);
}
g.strokePath();
}; };
b.aRolls.forEach((v, i) => drawDie(mx - 28, my - 26 + i * 40, v, 0xd83a3a));
b.dRolls.forEach((v, i) => drawDie(mx + 28, my - 26 + i * 40, v, 0xffffff)); // white outline pass, then colored fill pass
const res = this.add.text(mx, my + 64, `${b.aLoss} / ${b.dLoss}`, { drawPath(LINE_W + STROKE_W * 2, 0xffffff, 0.85);
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex, backgroundColor: '#000000aa', padding: { x: 6, y: 3 }, drawPath(LINE_W, color, 0.92);
}).setOrigin(0.5).setDepth(DEPTH.dice + 1); objs.push(res);
// arrowhead at the current tip, aligned to the curve tangent
if (t > 0.05) {
const prevT = Math.max(0, t - 0.04);
const tx = (1-t)*(1-t)*from.x + 2*(1-t)*t*cpx + t*t*to.x;
const ty = (1-t)*(1-t)*from.y + 2*(1-t)*t*cpy + t*t*to.y;
const px = (1-prevT)*(1-prevT)*from.x + 2*(1-prevT)*prevT*cpx + prevT*prevT*to.x;
const py = (1-prevT)*(1-prevT)*from.y + 2*(1-prevT)*prevT*cpy + prevT*prevT*to.y;
const adx = tx - px, ady = ty - py;
const aLen = Math.sqrt(adx * adx + ady * ady) || 1;
const ax = adx / aLen, ay = ady / aLen;
const s = STROKE_W;
// white outline triangle slightly larger
g.fillStyle(0xffffff, 0.85);
g.fillTriangle(
tx + ax * s, ty + ay * s,
tx - ax * (ARROW_LEN + s) + ay * (ARROW_WID + s), ty - ay * (ARROW_LEN + s) - ax * (ARROW_WID + s),
tx - ax * (ARROW_LEN + s) - ay * (ARROW_WID + s), ty - ay * (ARROW_LEN + s) + ax * (ARROW_WID + s),
);
// colored fill triangle
g.fillStyle(color, 0.95);
g.fillTriangle(
tx, ty,
tx - ax * ARROW_LEN + ay * ARROW_WID, ty - ay * ARROW_LEN - ax * ARROW_WID,
tx - ax * ARROW_LEN - ay * ARROW_WID, ty - ay * ARROW_LEN + ax * ARROW_WID,
);
}
};
await new Promise((resolve) => {
const progress = { t: 0 };
this.tweens.add({
targets: progress, t: 1, duration: 1000, ease: 'Sine.easeInOut',
onUpdate: () => drawArc(progress.t),
onComplete: () => { drawArc(1); resolve(); },
});
});
return g;
}
async animateBattle(b) {
if (!b) return;
const arrowGfx = await this.animateAttackArrow(b);
await this.animateBattleModal(b);
arrowGfx?.destroy();
}
async animateBattleModal(b) {
const attackerSeat = b.attackerSeat ?? this.gs.owner[b.from];
const defenderSeat = b.defenderSeat ?? this.gs.owner[b.to];
const attackerColor = this.colorOf(attackerSeat);
const defenderColor = this.colorOf(defenderSeat);
const attackerColorHex = this.colorHexOf(attackerSeat);
const defenderColorHex = this.colorHexOf(defenderSeat);
const attackerName = this.gs.players[attackerSeat].name;
const defenderName = this.gs.players[defenderSeat].name;
const W = 560, H = 320;
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
const startPos = this.boardToScreen(TERRITORIES[b.to].x, TERRITORIES[b.to].y);
// ── Container (all modal children use local coordinates) ──────────────────
const container = this.add.container(startPos.x, startPos.y).setDepth(DEPTH.popup).setScale(0.05);
// Background panel
const bg = this.add.graphics();
bg.fillStyle(0xf5f0e8, 0.97);
bg.fillRoundedRect(-W / 2, -H / 2, W, H, 14);
bg.lineStyle(3, COLORS.gold, 1);
bg.strokeRoundedRect(-W / 2, -H / 2, W, H, 14);
container.add(bg);
// Heading — three separate text objects so each territory name uses its owner's color
const headY = -H / 2 + 36;
const headStyle = { fontFamily: 'Righteous', fontSize: '22px' };
const tA = this.add.text(0, headY, TERRITORIES[b.from].name, { ...headStyle, color: attackerColorHex }).setOrigin(0.5);
const tSep = this.add.text(0, headY, ' ⚔ ', { ...headStyle, color: COLORS.goldHex }).setOrigin(0.5);
const tD = this.add.text(0, headY, TERRITORIES[b.to].name, { ...headStyle, color: defenderColorHex }).setOrigin(0.5);
// Reposition as a centered row now that widths are known
const totalW = tA.width + tSep.width + tD.width;
tA.setX( -totalW / 2 + tA.width / 2);
tSep.setX(-totalW / 2 + tA.width + tSep.width / 2);
tD.setX( -totalW / 2 + tA.width + tSep.width + tD.width / 2);
container.add([tA, tSep, tD]);
// Divider
const div = this.add.graphics();
div.lineStyle(1, COLORS.accent, 0.4);
div.lineBetween(-W / 2 + 20, -H / 2 + 64, W / 2 - 20, -H / 2 + 64);
container.add(div);
// Player name labels
container.add(this.add.text(-110, -H / 2 + 82, attackerName, {
fontFamily: '"Julius Sans One"', fontSize: '15px', color: attackerColorHex, align: 'center',
}).setOrigin(0.5));
container.add(this.add.text(110, -H / 2 + 82, defenderName, {
fontFamily: '"Julius Sans One"', fontSize: '15px', color: defenderColorHex, align: 'center',
}).setOrigin(0.5));
// ── Dice layout ──────────────────────────────────────────────────────────
const DIE = 52, STRIDE = 62;
const dieYs = (count) => {
const totalH = count * STRIDE - (STRIDE - DIE);
const top = -totalH / 2 + DIE / 2 + 30; // center in lower portion of modal
return Array.from({ length: count }, (_, i) => top + i * STRIDE);
};
const aYs = dieYs(b.aRolls.length);
const dYs = dieYs(b.dRolls.length);
const makeDie = (localX, localY, color) => {
const g = this.add.graphics();
g.fillStyle(color, 1);
g.fillRoundedRect(localX - DIE / 2, localY - DIE / 2, DIE, DIE, 8);
g.lineStyle(3, 0xffffff, 0.6);
g.strokeRoundedRect(localX - DIE / 2, localY - DIE / 2, DIE, DIE, 8);
const t = this.add.text(localX, localY, '?', {
fontFamily: 'Righteous', fontSize: '28px', color: '#ffffff',
}).setOrigin(0.5);
container.add([g, t]);
return { g, t, localX, localY };
};
const aDice = aYs.map((y) => makeDie(-110, y, attackerColor));
const dDice = dYs.map((y) => makeDie( 110, y, defenderColor));
// ── Zoom-in tween ─────────────────────────────────────────────────────────
await new Promise((resolve) => {
this.tweens.add({
targets: container, x: cx, y: cy, scaleX: 1, scaleY: 1,
duration: 1000, ease: 'Back.easeOut', onComplete: resolve,
});
});
// ── Dice rolling ─────────────────────────────────────────────────────────
playSound(this, SFX.DICE_ROLL);
for (let tick = 0; tick < 10; tick++) {
aDice.forEach((d) => d.t.setText(String(Math.ceil(Math.random() * 6))));
dDice.forEach((d) => d.t.setText(String(Math.ceil(Math.random() * 6))));
await this.delay(60);
}
aDice.forEach((d, i) => d.t.setText(String(b.aRolls[i])));
dDice.forEach((d, i) => d.t.setText(String(b.dRolls[i])));
// ── Pair-by-pair resolution arrows ────────────────────────────────────────
const pairs = Math.min(b.aRolls.length, b.dRolls.length);
const diceArrows = [];
for (let i = 0; i < pairs; i++) {
const aWins = b.aRolls[i] > b.dRolls[i];
const winCol = aWins ? attackerColor : defenderColor;
const fromPos = { x: cx + (aWins ? -110 : 110), y: cy + (aWins ? aYs[i] : dYs[i]) };
const toPos = { x: cx + (aWins ? 110 : -110), y: cy + (aWins ? dYs[i] : aYs[i]) };
const loser = aWins ? dDice[i] : aDice[i];
const ag = await this.animateDiceArrow(fromPos, toPos, winCol);
diceArrows.push(ag);
loser.g.setAlpha(0.22);
loser.t.setAlpha(0.22);
await this.delay(200);
}
if (b.conquered) playSound(this, SFX.SWORD_HIT); if (b.conquered) playSound(this, SFX.SWORD_HIT);
await this.delay(b.conquered ? 720 : 560); await this.delay(600);
objs.forEach((o) => o.destroy());
container.destroy(true);
diceArrows.forEach((g) => g.destroy());
}
async animateDiceArrow(from, to, color) {
const dx = to.x - from.x, dy = to.y - from.y;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
let perpX = -dy / len, perpY = dx / len;
if (perpY > 0) { perpX = -perpX; perpY = -perpY; }
const archH = Math.min(len * 0.5, 80);
const cpx = (from.x + to.x) / 2 + perpX * archH;
const cpy = (from.y + to.y) / 2 + perpY * archH;
const g = this.add.graphics().setDepth(DEPTH.popup + 2);
const LINE_W = 4, STROKE_W = 2, ARROW_LEN = 18, ARROW_WID = 9;
const drawArc = (t) => {
g.clear();
const steps = Math.max(2, Math.ceil(t * 48));
const pts = [];
for (let i = 0; i <= steps; i++) {
const tt = (i / steps) * t;
pts.push(
(1 - tt) * (1 - tt) * from.x + 2 * (1 - tt) * tt * cpx + tt * tt * to.x,
(1 - tt) * (1 - tt) * from.y + 2 * (1 - tt) * tt * cpy + tt * tt * to.y,
);
}
const drawPath = (w, col, alpha) => {
g.lineStyle(w, col, alpha);
g.beginPath();
for (let i = 0; i <= steps; i++) {
if (i === 0) g.moveTo(pts[i * 2], pts[i * 2 + 1]);
else g.lineTo(pts[i * 2], pts[i * 2 + 1]);
}
g.strokePath();
};
drawPath(LINE_W + STROKE_W * 2, 0xffffff, 0.85);
drawPath(LINE_W, color, 0.92);
if (t > 0.05) {
const prevT = Math.max(0, t - 0.04);
const tx = (1-t)*(1-t)*from.x + 2*(1-t)*t*cpx + t*t*to.x;
const ty = (1-t)*(1-t)*from.y + 2*(1-t)*t*cpy + t*t*to.y;
const px = (1-prevT)*(1-prevT)*from.x + 2*(1-prevT)*prevT*cpx + prevT*prevT*to.x;
const py = (1-prevT)*(1-prevT)*from.y + 2*(1-prevT)*prevT*cpy + prevT*prevT*to.y;
const adx = tx - px, ady = ty - py;
const aLen = Math.sqrt(adx * adx + ady * ady) || 1;
const ax = adx / aLen, ay = ady / aLen;
const s = STROKE_W;
g.fillStyle(0xffffff, 0.85);
g.fillTriangle(
tx + ax * s, ty + ay * s,
tx - ax * (ARROW_LEN + s) + ay * (ARROW_WID + s), ty - ay * (ARROW_LEN + s) - ax * (ARROW_WID + s),
tx - ax * (ARROW_LEN + s) - ay * (ARROW_WID + s), ty - ay * (ARROW_LEN + s) + ax * (ARROW_WID + s),
);
g.fillStyle(color, 0.95);
g.fillTriangle(
tx, ty,
tx - ax * ARROW_LEN + ay * ARROW_WID, ty - ay * ARROW_LEN - ax * ARROW_WID,
tx - ax * ARROW_LEN - ay * ARROW_WID, ty - ay * ARROW_LEN + ax * ARROW_WID,
);
}
};
await new Promise((resolve) => {
const progress = { t: 0 };
this.tweens.add({
targets: progress, t: 1, duration: 1000, ease: 'Sine.easeInOut',
onUpdate: () => drawArc(progress.t),
onComplete: () => { drawArc(1); resolve(); },
});
});
return g;
}
async animateConquest(fromId, toId, n, attackerSeat) {
const color = this.colorOf(attackerSeat);
const from = this.boardToScreen(TERRITORIES[fromId].x, TERRITORIES[fromId].y);
const to = this.boardToScreen(TERRITORIES[toId].x, TERRITORIES[toId].y);
const dx = to.x - from.x, dy = to.y - from.y;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
const ax = dx / len, ay = dy / len;
const LINE_W = 6, STROKE_W = 3, ARROW_LEN = 26, ARROW_WID = 12;
const g = this.add.graphics().setDepth(DEPTH.badge - 1);
const drawLine = (t) => {
g.clear();
const tipX = from.x + dx * t, tipY = from.y + dy * t;
g.lineStyle(LINE_W + STROKE_W * 2, 0xffffff, 0.85);
g.beginPath(); g.moveTo(from.x, from.y); g.lineTo(tipX, tipY); g.strokePath();
g.lineStyle(LINE_W, color, 0.92);
g.beginPath(); g.moveTo(from.x, from.y); g.lineTo(tipX, tipY); g.strokePath();
if (t > 0.05) {
const s = STROKE_W;
g.fillStyle(0xffffff, 0.85);
g.fillTriangle(
tipX + ax * s, tipY + ay * s,
tipX - ax * (ARROW_LEN + s) + ay * (ARROW_WID + s), tipY - ay * (ARROW_LEN + s) - ax * (ARROW_WID + s),
tipX - ax * (ARROW_LEN + s) - ay * (ARROW_WID + s), tipY - ay * (ARROW_LEN + s) + ax * (ARROW_WID + s),
);
g.fillStyle(color, 0.95);
g.fillTriangle(
tipX, tipY,
tipX - ax * ARROW_LEN + ay * ARROW_WID, tipY - ay * ARROW_LEN - ax * ARROW_WID,
tipX - ax * ARROW_LEN - ay * ARROW_WID, tipY - ay * ARROW_LEN + ax * ARROW_WID,
);
}
};
// Phase 1: draw straight line with arrow over 1 s
await new Promise((resolve) => {
const progress = { t: 0 };
this.tweens.add({
targets: progress, t: 1, duration: 1000, ease: 'Sine.easeInOut',
onUpdate: () => drawLine(progress.t),
onComplete: () => { drawLine(1); resolve(); },
});
});
// Phase 2: army count floats rise over 1 s while arrow stays visible
const lossText = this.add.text(from.x, from.y - BADGE_R - 8, `${n}`, {
fontFamily: 'Righteous', fontSize: '26px', color: COLORS.dangerHex,
stroke: '#000000', strokeThickness: 3,
}).setOrigin(0.5).setDepth(DEPTH.label + 1);
const gainText = this.add.text(to.x, to.y - BADGE_R - 8, `+${n}`, {
fontFamily: 'Righteous', fontSize: '26px', color: '#4fd06a',
stroke: '#000000', strokeThickness: 3,
}).setOrigin(0.5).setDepth(DEPTH.label + 1);
await new Promise((resolve) => {
this.tweens.add({
targets: [lossText, gainText], y: '-=55', duration: 1000,
ease: 'Sine.easeOut', onComplete: resolve,
});
});
// Phase 3: fade everything out
await new Promise((resolve) => {
this.tweens.add({ targets: g, alpha: 0, duration: 400, ease: 'Linear' });
this.tweens.add({
targets: [lossText, gainText], alpha: 0, duration: 400,
ease: 'Linear', onComplete: resolve,
});
});
g.destroy();
lossText.destroy();
gainText.destroy();
} }
// ── turn loop ─────────────────────────────────────────────────────────────────── // ── turn loop ───────────────────────────────────────────────────────────────────
@ -592,8 +936,15 @@ export default class RiskGame extends Phaser.Scene {
if (!atk) { this.gs = endAttack(this.gs); break; } if (!atk) { this.gs = endAttack(this.gs); break; }
this.gs = resolveAttack(this.gs, atk.from, atk.to, atk.numDice); this.gs = resolveAttack(this.gs, atk.from, atk.to, atk.numDice);
await this.animateBattle(this.gs.lastBattle); await this.animateBattle(this.gs.lastBattle);
if (this.gs.pendingConquest) this.gs = advanceArmies(this.gs, chooseAdvance(this.gs, seat, skill)); 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(); this.render();
await this.animateConquest(cFrom, cTo, n, seat);
} else {
this.render();
}
if (isGameOver(this.gs)) { this.setPortraitThinking(seat, false); return; } if (isGameOver(this.gs)) { this.setPortraitThinking(seat, false); return; }
await this.delay(200); await this.delay(200);
} }

View File

@ -234,7 +234,7 @@ export function resolveAttack(s0, from, to, numDice) {
s.armies[to] -= dLoss; s.armies[to] -= dLoss;
const conquered = s.armies[to] <= 0; const conquered = s.armies[to] <= 0;
s.lastBattle = { from, to, aRolls, dRolls, aLoss, dLoss, conquered }; s.lastBattle = { from, to, aRolls, dRolls, aLoss, dLoss, conquered, attackerSeat: seat, defenderSeat: s.owner[to] };
if (conquered) { if (conquered) {
const defender = s.owner[to]; const defender = s.owner[to];