diff --git a/public/assets/fx/jello-monsters.mp3 b/public/assets/fx/jello-monsters.mp3 new file mode 100644 index 0000000..88824c6 Binary files /dev/null and b/public/assets/fx/jello-monsters.mp3 differ diff --git a/public/assets/fx/squash.mp3 b/public/assets/fx/squash.mp3 new file mode 100644 index 0000000..9f71755 Binary files /dev/null and b/public/assets/fx/squash.mp3 differ diff --git a/public/assets/fx/squish.mp3 b/public/assets/fx/squish.mp3 new file mode 100644 index 0000000..c355ddb Binary files /dev/null and b/public/assets/fx/squish.mp3 differ diff --git a/public/assets/fx/woosh.mp3 b/public/assets/fx/woosh.mp3 new file mode 100644 index 0000000..11eb80f Binary files /dev/null and b/public/assets/fx/woosh.mp3 differ diff --git a/public/src/games/puddingmonsters/PuddingMonstersGame.js b/public/src/games/puddingmonsters/PuddingMonstersGame.js index 67dc568..911ac72 100644 --- a/public/src/games/puddingmonsters/PuddingMonstersGame.js +++ b/public/src/games/puddingmonsters/PuddingMonstersGame.js @@ -56,6 +56,10 @@ export default class PuddingMonstersGame extends Phaser.Scene { this.drag = null; this.selectedCell = null; this.animGfx = null; + this.eyeOffsets = new Map(); + this.eyeTimers = []; + this.eyeAnimActive = false; + this.blobGfx = []; } async create() { @@ -89,8 +93,12 @@ export default class PuddingMonstersGame extends Phaser.Scene { } clearLayer() { + this.eyeAnimActive = false; + this.eyeTimers.forEach((t) => t.remove(false)); + this.eyeTimers = []; + this.eyeOffsets = new Map(); this.layer.removeAll(true); - this.monsterGfx = null; + this.blobGfx = []; this.targetGfx = null; this.animGfx = null; this.undoBtn = null; @@ -262,12 +270,65 @@ export default class PuddingMonstersGame extends Phaser.Scene { this.clearLayer(); this.computeLayout(); this.buildBoard(); - this.monsterGfx = this.add.graphics().setDepth(D.monster); this.layer.add(this.monsterGfx); this.targetGfx = this.add.graphics().setDepth(D.target); this.layer.add(this.targetGfx); this.drawAllMonsters(); this.drawTargets(); this.drawHud(); this.updateHud(); + this.initEyeOffsets(); + this.startEyeAnimation(); + } + + initEyeOffsets() { + const er = this.cell * 0.105; + const maxR = er * 0.42; + this.eyeOffsets = new Map(); + for (const blob of this.state.blobs) { + for (const [, , m] of blob.cells) { + if (!this.eyeOffsets.has(m)) { + const angle = Math.random() * Math.PI * 2; + const r = Math.random() * maxR; + this.eyeOffsets.set(m, { dx: Math.cos(angle) * r, dy: Math.sin(angle) * r }); + } + } + } + } + + startEyeAnimation() { + const er = this.cell * 0.105; + const maxR = er * 0.42; + this.eyeAnimActive = true; + + const scheduleNext = (m) => { + if (!this.eyeAnimActive) return; + const delay = 1000 + Math.random() * 2500; + const timer = this.time.delayedCall(delay, () => { + if (!this.eyeAnimActive || !this.eyeOffsets.has(m)) return; + const angle = Math.random() * Math.PI * 2; + const r = Math.random() * maxR; + const cur = this.eyeOffsets.get(m); + const proxy = { dx: cur.dx, dy: cur.dy }; + this.tweens.add({ + targets: proxy, dx: Math.cos(angle) * r, dy: Math.sin(angle) * r, + duration: 120 + Math.random() * 80, + ease: 'Sine.easeInOut', + onUpdate: () => { + if (!this.eyeAnimActive || !this.blobGfx.length) return; + this.eyeOffsets.set(m, { dx: proxy.dx, dy: proxy.dy }); + if (!this.busy && !this.overlayUp) this.drawAllMonsters(); + }, + onComplete: () => scheduleNext(m), + }); + }); + this.eyeTimers.push(timer); + }; + + let stagger = 0; + for (const m of this.eyeOffsets.keys()) { + const t = this.time.delayedCall(stagger + Math.random() * 600, () => scheduleNext(m)); + this.eyeTimers.push(t); + stagger += 200; + } } computeLayout() { @@ -371,12 +432,42 @@ export default class PuddingMonstersGame extends Phaser.Scene { } drawAllMonsters(excludeIdx = -1) { - const g = this.monsterGfx; - g.clear(); + // Destroy previous per-blob containers (their tweens are auto-removed on destroy) + this.blobGfx.forEach(({ container }) => container.destroy()); + this.blobGfx = []; + this.state.blobs.forEach((blob, i) => { if (i === excludeIdx) return; const sel = this.selectedCell && blobAt(this.state, this.selectedCell.x, this.selectedCell.y) === i; - this.drawBlobInto(g, blob.cells, 0, 0, sel); + + // Position the container at the blob centroid so scaling pivots from the centre + let cx = 0, cy = 0; + for (const [bx, by] of blob.cells) { cx += this.cellCx(bx); cy += this.cellCy(by); } + cx /= blob.cells.length; cy /= blob.cells.length; + + const container = this.add.container(cx, cy).setDepth(D.monster); + const g = this.add.graphics(); + this.drawBlobInto(g, blob.cells, -cx, -cy, sel); + container.add(g); + this.layer.add(container); + + // Squash-and-stretch pulse: even/odd blobs start at opposite phases so they + // never all peak simultaneously. Duration varies per blob to drift over time. + const a = 0.015; + const startX = i % 2 === 0 ? 1 + a : 1 - a; + const startY = i % 2 === 0 ? 1 - a : 1 + a; + container.setScale(startX, startY); + this.tweens.add({ + targets: container, + scaleX: 1 - a, + scaleY: 1 + a, + duration: 900 + i * 310, + ease: 'Sine.easeInOut', + yoyo: true, + repeat: -1, + }); + + this.blobGfx.push({ container, blobIdx: i }); }); } @@ -398,13 +489,14 @@ export default class PuddingMonstersGame extends Phaser.Scene { // every monster keeps its face inside the merged blob: eyes on every cell const er = c * 0.105, sp = c * 0.155; - for (const [x, y] of cells) { + for (const [x, y, m] of cells) { + const eo = this.eyeOffsets?.get(m) ?? { dx: 0, dy: 0 }; const ex = this.cellCx(x) + ox, ey = this.cellCy(y) + oy - c * 0.04; g.fillStyle(0xffffff, 1); g.fillCircle(ex - sp, ey, er); g.fillCircle(ex + sp, ey, er); g.fillStyle(0x1a1a1a, 1); - g.fillCircle(ex - sp + er * 0.25, ey + er * 0.1, er * 0.5); - g.fillCircle(ex + sp + er * 0.25, ey + er * 0.1, er * 0.5); + g.fillCircle(ex - sp + eo.dx, ey + eo.dy, er * 0.5); + g.fillCircle(ex + sp + eo.dx, ey + eo.dy, er * 0.5); } } @@ -449,10 +541,10 @@ export default class PuddingMonstersGame extends Phaser.Scene { }).setOrigin(0, 0.5).setDepth(D.ui); this.layer.add(title); - this.movesText = this.add.text(GAME_WIDTH - 50, 80, '', { + this.movesText = this.add.text(GAME_WIDTH - 50, 155, '', { fontFamily: 'Righteous', fontSize: '30px', color: COLORS.textHex, }).setOrigin(1, 0.5).setDepth(D.ui); - this.starsText = this.add.text(GAME_WIDTH - 50, 124, '', { + this.starsText = this.add.text(GAME_WIDTH - 50, 199, '', { fontFamily: 'serif', fontSize: '30px', color: '#ffd54a', }).setOrigin(1, 0.5).setDepth(D.ui); this.layer.add([this.movesText, this.starsText]); @@ -545,7 +637,7 @@ export default class PuddingMonstersGame extends Phaser.Scene { const tgtY = dy * animSteps * this.cell; const drawAt = (t) => { this.animGfx.clear(); this.drawBlobInto(this.animGfx, movingCells, tgtX * t, tgtY * t); }; drawAt(0); - playSound(this, SFX.PIECE_CLICK); + playSound(this, SFX.WOOSH); this.tweens.add({ targets: proxy, v: 1, @@ -569,16 +661,17 @@ export default class PuddingMonstersGame extends Phaser.Scene { const restIdx = blobAt(this.state, this.selectedCell.x, this.selectedCell.y); const finish = () => { - if (!this.monsterGfx) return; // scene view changed mid-animation + if (this.view !== 'play') return; // scene view changed mid-animation this.drawAllMonsters(); this.busy = false; if (this.state.state === 'won') this.onSolved(); }; if (restIdx < 0) { finish(); return; } if (res.merged) { - playSound(this, SFX.CARD_PLACE); + playSound(this, SFX.SQUASH); this.wobbleBlob(restIdx, 1.16, 0.84, finish); // jelly merge jiggle } else { + playSound(this, SFX.SQUISH); const sx = dx !== 0 ? 0.9 : 1.1; // impact squash along the slide axis this.wobbleBlob(restIdx, sx, 2 - sx, finish, true); } @@ -626,11 +719,14 @@ export default class PuddingMonstersGame extends Phaser.Scene { } nudgeInvalid(idx) { - // brief wobble of the whole monster layer to signal "can't move" - const blob = this.state.blobs[idx]; - const cx = this.cellCx(repCell(blob)[0]); - void cx; - this.tweens.add({ targets: this.monsterGfx, x: 6, duration: 50, yoyo: true, repeat: 1, onComplete: () => { this.monsterGfx.x = 0; } }); + const entry = this.blobGfx.find((b) => b.blobIdx === idx); + if (!entry) return; + const { container } = entry; + const origX = container.x; + this.tweens.add({ + targets: container, x: origX + 6, duration: 50, yoyo: true, repeat: 1, + onComplete: () => { container.x = origX; }, + }); } undo() { @@ -722,7 +818,7 @@ export default class PuddingMonstersGame extends Phaser.Scene { api.post('/history/single-player', { slug: 'puddingmonsters', score: this.moves, opponentScores: [], result: 'win', }).catch(() => { /* best effort */ }); - playSound(this, SFX.VICTORY_SHORT); + playSound(this, SFX.JELLO_MONSTERS); const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; diff --git a/public/src/scenes/PreloadScene.js b/public/src/scenes/PreloadScene.js index 3c9616a..135c480 100644 --- a/public/src/scenes/PreloadScene.js +++ b/public/src/scenes/PreloadScene.js @@ -119,6 +119,10 @@ export default class PreloadScene extends Phaser.Scene { 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.audio('sfx-squish', '/assets/fx/squish.mp3'); + this.load.audio('sfx-squash', '/assets/fx/squash.mp3'); + this.load.audio('sfx-woosh', '/assets/fx/woosh.mp3'); + this.load.audio('sfx-jello-monsters','/assets/fx/jello-monsters.mp3'); this.load.spritesheet('catan-special-cards', '/assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 }); diff --git a/public/src/ui/Sounds.js b/public/src/ui/Sounds.js index 0c8a56a..0764d0f 100644 --- a/public/src/ui/Sounds.js +++ b/public/src/ui/Sounds.js @@ -44,6 +44,10 @@ export const SFX = { EIGHTBIT_ACTIVATE: 'sfx-8bit-activate', EIGHTBIT_ACTION: 'sfx-8bit-action', EIGHTBIT_MOVE: 'sfx-8bit-move', + SQUISH: 'sfx-squish', + SQUASH: 'sfx-squash', + WOOSH: 'sfx-woosh', + JELLO_MONSTERS: 'sfx-jello-monsters', }; export function playSound(scene, key) {