Polish on Forbidden Island
This commit is contained in:
parent
88dcaf8e15
commit
ab84b32f1d
Binary file not shown.
|
|
@ -1770,6 +1770,10 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
|
||||
// DOM video for AI opponent (floated on top of sprite)
|
||||
let domVid = null;
|
||||
let vizCanvas = null;
|
||||
let vizRafId = null; // for cleanup on modal close
|
||||
let startVisualizer = () => {}; // no-op default, overridden below for AI
|
||||
let stopVisualizer = () => {}; // no-op default, overridden below for AI
|
||||
if (opp) {
|
||||
const vidEl = document.createElement('video');
|
||||
vidEl.muted = true; vidEl.loop = true; vidEl.playsInline = true; vidEl.autoplay = true;
|
||||
|
|
@ -1779,6 +1783,66 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
vidEl.addEventListener('error', () => { vidEl.style.display = 'none'; }, { once: true });
|
||||
vidEl.play().catch(() => {});
|
||||
domVid = reg(this.add.dom(portraitCX, portraitCY - 20, vidEl).setDepth(D + 3));
|
||||
|
||||
// Speech visualizer canvas — animated bars overlay during intro speech
|
||||
vizCanvas = document.createElement('canvas');
|
||||
vizCanvas.width = size; vizCanvas.height = size;
|
||||
vizCanvas.style.cssText = `width:${size}px;height:${size}px;border-radius:50%;pointer-events:none;opacity:0;transition:opacity 0.2s ease;`;
|
||||
const vizDom = reg(this.add.dom(portraitCX, portraitCY - 20, vizCanvas).setDepth(D + 4));
|
||||
const vctx = vizCanvas.getContext('2d');
|
||||
const BAR_COUNT = 6;
|
||||
const bars = Array.from({ length: BAR_COUNT }, () => ({ cur: 0.1, target: 0.5 }));
|
||||
let vizActive = false; let retargetTimer = null;
|
||||
let vizColor = [0, 200, 255];
|
||||
|
||||
function _vizFrame() {
|
||||
if (!vizActive) { vizRafId = null; vctx.clearRect(0, 0, size, size); return; }
|
||||
vctx.clearRect(0, 0, size, size);
|
||||
const gy = size * 0.58;
|
||||
const grad = vctx.createLinearGradient(0, gy, 0, size);
|
||||
grad.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
grad.addColorStop(1, 'rgba(0,0,0,0.38)');
|
||||
vctx.fillStyle = grad;
|
||||
vctx.fillRect(0, gy, size, size - gy);
|
||||
const [r, g, b] = vizColor;
|
||||
const barW = size * 0.072;
|
||||
const gap = size * 0.03;
|
||||
const totalW = BAR_COUNT * barW + (BAR_COUNT - 1) * gap;
|
||||
const startX = (size - totalW) / 2;
|
||||
const maxH = size * 0.36;
|
||||
const baseY = size * 0.88;
|
||||
bars.forEach((bar, i) => {
|
||||
bar.cur += (bar.target - bar.cur) * 0.14;
|
||||
const h = Math.max(bar.cur * maxH, size * 0.04);
|
||||
const x = startX + i * (barW + gap);
|
||||
vctx.shadowColor = `rgba(${r},${g},${b},0.7)`;
|
||||
vctx.shadowBlur = 5;
|
||||
vctx.fillStyle = `rgba(${r},${g},${b},0.88)`;
|
||||
vctx.beginPath();
|
||||
vctx.roundRect(x, baseY - h, barW, h, 2);
|
||||
vctx.fill();
|
||||
});
|
||||
vctx.shadowBlur = 0;
|
||||
vizRafId = requestAnimationFrame(_vizFrame);
|
||||
}
|
||||
|
||||
startVisualizer = function() {
|
||||
vizColor = [0, 200, 255];
|
||||
vizActive = true;
|
||||
vizCanvas.style.opacity = '1';
|
||||
if (!retargetTimer) {
|
||||
retargetTimer = setInterval(() => {
|
||||
bars.forEach(bb => { bb.target = 0.2 + Math.random() * 0.8; });
|
||||
}, 120);
|
||||
}
|
||||
if (!vizRafId) vizRafId = requestAnimationFrame(_vizFrame);
|
||||
}
|
||||
|
||||
stopVisualizer = function() {
|
||||
vizActive = false;
|
||||
vizCanvas.style.opacity = '0';
|
||||
clearInterval(retargetTimer); retargetTimer = null;
|
||||
}
|
||||
} else {
|
||||
// Human player — role-colored avatar circle
|
||||
const avG = reg(this.add.graphics().setDepth(D + 2));
|
||||
|
|
@ -1791,10 +1855,10 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
}).setOrigin(0.5).setDepth(D + 3));
|
||||
}
|
||||
|
||||
// Intro speech for AI opponents
|
||||
// Intro speech for AI opponents (with visual overlay)
|
||||
if (opp?.speech?.intro?.length) {
|
||||
const clip = opp.speech.intro[Math.floor(Math.random() * opp.speech.intro.length)];
|
||||
enqueueSpeech(clip);
|
||||
enqueueSpeech(clip, { onStart: startVisualizer, onEnd: stopVisualizer });
|
||||
}
|
||||
|
||||
// Player name below portrait
|
||||
|
|
@ -1886,6 +1950,8 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
|||
// ── Next / Let's Play button ─────────────────────────────────────────────
|
||||
const btnLabel = isLast ? "Let's Play!" : 'Next';
|
||||
const btn = new Button(this, cx + pw / 2 - 140, py + ph - 44, btnLabel, () => {
|
||||
stopVisualizer();
|
||||
if (vizRafId) { cancelAnimationFrame(vizRafId); vizRafId = null; }
|
||||
if (domVid?.node) { try { domVid.node.pause(); domVid.node.src = ''; } catch { /* ignore */ } }
|
||||
resetSpeechQueue();
|
||||
for (const o of objs) { try { o.destroy(); } catch { /* ignore */ } }
|
||||
|
|
|
|||
|
|
@ -471,6 +471,10 @@ export function resolveFlood(state) {
|
|||
const count = floodDrawCount(s.waterLevel);
|
||||
const rng = makeRng(s);
|
||||
|
||||
// Buffer cards flooded this phase; only merge into floodDiscard after all
|
||||
// draws complete so mid-phase reshuffles can't redraw a card from this phase.
|
||||
const phaseDiscard = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (s.floodDeck.length === 0) {
|
||||
if (s.floodDiscard.length === 0) break;
|
||||
|
|
@ -481,7 +485,7 @@ export function resolveFlood(state) {
|
|||
const tile = s.tiles[id];
|
||||
if (tile.state === 'dry') {
|
||||
tile.state = 'flooded';
|
||||
s.floodDiscard.push(id);
|
||||
phaseDiscard.push(id);
|
||||
s.log.push({ kind: 'flood', tileId: id });
|
||||
} else if (tile.state === 'flooded') {
|
||||
tile.state = 'sunk';
|
||||
|
|
@ -489,6 +493,10 @@ export function resolveFlood(state) {
|
|||
// Card is removed from the game (not discarded) since the tile is gone.
|
||||
}
|
||||
}
|
||||
|
||||
// Merge phase discards now that all draws are done — ensures a mid-phase
|
||||
// reshuffle only contains cards from previous phases, not this one.
|
||||
s.floodDiscard.push(...phaseDiscard);
|
||||
rng.commit();
|
||||
|
||||
// Forced swims for any pawn whose tile has sunk.
|
||||
|
|
|
|||
Loading…
Reference in New Issue