feat: add 8-bit sound effects and improve Battleship impact visuals

- Add 4 new 8-bit sound effects (select, activate, action, move) to SFX system
- Wire 8-bit sounds to Risk attack, fortify, and movement animations
- Add visual impact markers for hits and misses in Battleship
- Fix Battleship portrait texture key reference
- Improve ship drag hit area with geometric rectangle
- Add phase guard and tween cancellation on ship drag start
This commit is contained in:
Brian Fertig 2026-06-18 23:36:01 -06:00
parent 546e1de240
commit 54a1ba2280
7 changed files with 41 additions and 11 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -216,7 +216,7 @@ export default class BattleshipGame extends Phaser.Scene {
// Player — bottom-right margin.
this.plrX = RX + GRID + (GAME_WIDTH - (RX + GRID)) / 2;
this.plrY = 740;
createPlayerPortrait(this, this.plrX, this.plrY, r, DEPTH.ui, 'Battleship');
createPlayerPortrait(this, this.plrX, this.plrY, r, DEPTH.ui, 'BattleshipGame');
this.add.text(this.plrX, this.plrY - r - 12, auth.user?.username ?? 'You', {
fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex,
wordWrap: { width: 240 }, align: 'center',
@ -373,11 +373,7 @@ export default class BattleshipGame extends Phaser.Scene {
const w = piece.horizontal ? piece.spec.len * CELL : CELL;
const h = piece.horizontal ? CELL : piece.spec.len * CELL;
piece.container.setSize(w, h);
// Our hull graphics are top-left anchored (local 0,0 → w,h), but Phaser's
// input normalizes the hit test by a Container's displayOrigin, which is
// hardcoded to (width/2, height/2). Offset the hit area by the same amount
// so the clickable zone lines up with the visible ship instead of sitting
// up-and-left of it.
piece.container.removeInteractive();
piece.container.setInteractive(
new Phaser.Geom.Rectangle(w / 2, h / 2, w, h), Phaser.Geom.Rectangle.Contains);
this.input.setDraggable(piece.container);
@ -423,6 +419,8 @@ export default class BattleshipGame extends Phaser.Scene {
}
onShipDragStart(piece) {
if (this.gs.phase !== 'placement') return;
this.tweens.killTweensOf(piece.container);
this.activeShip = piece;
if (piece.placed) { piece.placed = false; this.syncFleetFromPieces(); this.updateReadyButton(); }
piece.container.setScale(1).setDepth(DEPTH.missile);
@ -745,10 +743,26 @@ export default class BattleshipGame extends Phaser.Scene {
if (res.result === 'sunk') this.onShipSunk(res.ship, firedByPlayer);
if (this.opponentPortrait) this.playOppEmotion(firedByPlayer ? 'upset' : 'happy');
}
this.renderMarkers();
this.drawImpactMarker(firedByPlayer, res);
this.refreshFleetPanels();
}
drawImpactMarker(firedByPlayer, res) {
const g = firedByPlayer ? this.enemyMarkers : this.yourMarkers;
const ox = firedByPlayer ? EX : RX;
const x = ox + res.c * CELL + CELL / 2;
const y = BY + res.r * CELL + CELL / 2;
if (res.result === 'miss') {
g.fillStyle(0x0a1a22, 0.5); g.fillCircle(x, y, 9);
g.fillStyle(C.miss, 0.95); g.fillCircle(x, y, 7);
g.lineStyle(1, 0x88a6b2, 0.8); g.strokeCircle(x, y, 7);
} else {
g.fillStyle(C.hit, 0.25); g.fillCircle(x, y, 16);
g.fillStyle(C.hit, 1); g.fillCircle(x, y, 10);
g.fillStyle(C.hitGlow, 1); g.fillCircle(x, y, 5);
}
}
splash(x, y) {
const ripple = this.add.graphics().setDepth(DEPTH.fx);
const prox = { p: 0 };

View File

@ -389,21 +389,27 @@ export default class RiskGame extends Phaser.Scene {
if (s.phase === 'attack') {
if (s.owner[id] === seat && s.armies[id] >= 2 && ADJ[id].some((t) => s.owner[t] !== seat)) {
playSound(this, SFX.EIGHTBIT_SELECT);
this.selFrom = id; this.render(); return;
}
if (this.selFrom != null && canAttack(s, seat, this.selFrom, id)) { this.doHumanAttack(this.selFrom, id); return; }
if (this.selFrom != null && canAttack(s, seat, this.selFrom, id)) {
playSound(this, SFX.EIGHTBIT_ACTIVATE);
this.doHumanAttack(this.selFrom, id); return;
}
this.selFrom = null; this.render(); return;
}
if (s.phase === 'fortify') {
if (this.selFrom == null) {
if (s.owner[id] === seat && s.armies[id] >= 2 && connectedOwned(s, seat, id).length > 0) {
playSound(this, SFX.EIGHTBIT_SELECT);
this.selFrom = id; this.render();
}
return;
}
if (id === this.selFrom) { this.selFrom = null; this.render(); return; }
if (s.owner[id] === seat && connectedOwned(s, seat, this.selFrom).includes(id)) {
playSound(this, SFX.EIGHTBIT_ACTION);
const from = this.selFrom, to = id;
this.openMoveModal(1, s.armies[from] - 1, s.armies[from] - 1, async (n) => {
this.selFrom = null;
@ -937,8 +943,9 @@ export default class RiskGame extends Phaser.Scene {
const progress = { t: 0 };
this.tweens.add({
targets: progress, t: 1, duration: 1000, ease: 'Sine.easeInOut',
onUpdate: () => drawArc(progress.t),
onComplete: () => { drawArc(1); resolve(); },
onStart: () => playSound(this, SFX.BATTLESHIP_LAUNCH),
onUpdate: () => drawArc(progress.t),
onComplete: () => { drawArc(1); playSound(this, SFX.BATTLESHIP_HIT); resolve(); },
});
});
@ -986,7 +993,8 @@ export default class RiskGame extends Phaser.Scene {
const progress = { t: 0 };
this.tweens.add({
targets: progress, t: 1, duration: 1000, ease: 'Sine.easeInOut',
onUpdate: () => drawLine(progress.t),
onStart: () => playSound(this, SFX.EIGHTBIT_MOVE),
onUpdate: () => drawLine(progress.t),
onComplete: () => { drawLine(1); resolve(); },
});
});

View File

@ -115,6 +115,10 @@ export default class PreloadScene extends Phaser.Scene {
this.load.audio('sfx-scifi-woosh', '/assets/fx/scifi-woosh.mp3');
this.load.audio('sfx-scifi-plink', '/assets/fx/scifi-plink.mp3');
this.load.audio('sfx-scifi-plonk', '/assets/fx/scifi-plonk.mp3');
this.load.audio('sfx-8bit-select', '/assets/fx/8bit-select.mp3');
this.load.audio('sfx-8bit-activate', '/assets/fx/8bit-activate.mp3');
this.load.audio('sfx-8bit-action', '/assets/fx/8bit-action.mp3');
this.load.audio('sfx-8bit-move', '/assets/fx/8bit-move.mp3');
this.load.spritesheet('catan-special-cards', '/assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 });

View File

@ -40,6 +40,10 @@ export const SFX = {
MONOPOLY_EXPENSE: 'sfx-monopoly-expense',
MONOPAY: 'sfx-monopoly-pay',
MONOPOLY_PAID: 'sfx-monopoly-paid',
EIGHTBIT_SELECT: 'sfx-8bit-select',
EIGHTBIT_ACTIVATE: 'sfx-8bit-activate',
EIGHTBIT_ACTION: 'sfx-8bit-action',
EIGHTBIT_MOVE: 'sfx-8bit-move',
};
export function playSound(scene, key) {