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)
|
// DOM video for AI opponent (floated on top of sprite)
|
||||||
let domVid = null;
|
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) {
|
if (opp) {
|
||||||
const vidEl = document.createElement('video');
|
const vidEl = document.createElement('video');
|
||||||
vidEl.muted = true; vidEl.loop = true; vidEl.playsInline = true; vidEl.autoplay = true;
|
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.addEventListener('error', () => { vidEl.style.display = 'none'; }, { once: true });
|
||||||
vidEl.play().catch(() => {});
|
vidEl.play().catch(() => {});
|
||||||
domVid = reg(this.add.dom(portraitCX, portraitCY - 20, vidEl).setDepth(D + 3));
|
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 {
|
} else {
|
||||||
// Human player — role-colored avatar circle
|
// Human player — role-colored avatar circle
|
||||||
const avG = reg(this.add.graphics().setDepth(D + 2));
|
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));
|
}).setOrigin(0.5).setDepth(D + 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intro speech for AI opponents
|
// Intro speech for AI opponents (with visual overlay)
|
||||||
if (opp?.speech?.intro?.length) {
|
if (opp?.speech?.intro?.length) {
|
||||||
const clip = opp.speech.intro[Math.floor(Math.random() * 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
|
// Player name below portrait
|
||||||
|
|
@ -1886,6 +1950,8 @@ export default class ForbiddenIslandGame extends Phaser.Scene {
|
||||||
// ── Next / Let's Play button ─────────────────────────────────────────────
|
// ── Next / Let's Play button ─────────────────────────────────────────────
|
||||||
const btnLabel = isLast ? "Let's Play!" : 'Next';
|
const btnLabel = isLast ? "Let's Play!" : 'Next';
|
||||||
const btn = new Button(this, cx + pw / 2 - 140, py + ph - 44, btnLabel, () => {
|
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 */ } }
|
if (domVid?.node) { try { domVid.node.pause(); domVid.node.src = ''; } catch { /* ignore */ } }
|
||||||
resetSpeechQueue();
|
resetSpeechQueue();
|
||||||
for (const o of objs) { try { o.destroy(); } catch { /* ignore */ } }
|
for (const o of objs) { try { o.destroy(); } catch { /* ignore */ } }
|
||||||
|
|
|
||||||
|
|
@ -471,6 +471,10 @@ export function resolveFlood(state) {
|
||||||
const count = floodDrawCount(s.waterLevel);
|
const count = floodDrawCount(s.waterLevel);
|
||||||
const rng = makeRng(s);
|
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++) {
|
for (let i = 0; i < count; i++) {
|
||||||
if (s.floodDeck.length === 0) {
|
if (s.floodDeck.length === 0) {
|
||||||
if (s.floodDiscard.length === 0) break;
|
if (s.floodDiscard.length === 0) break;
|
||||||
|
|
@ -481,7 +485,7 @@ export function resolveFlood(state) {
|
||||||
const tile = s.tiles[id];
|
const tile = s.tiles[id];
|
||||||
if (tile.state === 'dry') {
|
if (tile.state === 'dry') {
|
||||||
tile.state = 'flooded';
|
tile.state = 'flooded';
|
||||||
s.floodDiscard.push(id);
|
phaseDiscard.push(id);
|
||||||
s.log.push({ kind: 'flood', tileId: id });
|
s.log.push({ kind: 'flood', tileId: id });
|
||||||
} else if (tile.state === 'flooded') {
|
} else if (tile.state === 'flooded') {
|
||||||
tile.state = 'sunk';
|
tile.state = 'sunk';
|
||||||
|
|
@ -489,6 +493,10 @@ export function resolveFlood(state) {
|
||||||
// Card is removed from the game (not discarded) since the tile is gone.
|
// 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();
|
rng.commit();
|
||||||
|
|
||||||
// Forced swims for any pawn whose tile has sunk.
|
// Forced swims for any pawn whose tile has sunk.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue