diff --git a/assets/fx/zuma-lose.mp3 b/assets/fx/zuma-lose.mp3 new file mode 100644 index 0000000..4f0d9bf Binary files /dev/null and b/assets/fx/zuma-lose.mp3 differ diff --git a/data/zuma-music.json b/data/zuma-music.json index dea1de3..3757674 100644 --- a/data/zuma-music.json +++ b/data/zuma-music.json @@ -16,5 +16,5 @@ "title": "Deep in the Jungle" } ], - "volume": 0.3 + "volume": 0.5 } diff --git a/data/zuma.json b/data/zuma.json index 4ac30c4..40a47a1 100644 --- a/data/zuma.json +++ b/data/zuma.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-07-28T21:55:21.106Z", + "generatedAt": "2026-07-28T23:39:54.596Z", "count": 20, "levels": [ { @@ -372,13 +372,14 @@ "colors": 4, "quota": 22, "introBalls": 7, - "pushSpeed": 100, + "pushSpeed": 60, "powerUpRate": 0.07, + "background": "zuma-background-1", "seed": 8919, "starScores": [ - 750, - 1130, - 1500 + 660, + 990, + 1320 ] }, { @@ -527,13 +528,14 @@ "colors": 4, "quota": 26, "introBalls": 8, - "pushSpeed": 128, + "pushSpeed": 76, "powerUpRate": 0.07, + "background": "zuma-background-2", "seed": 16838, "starScores": [ - 960, - 1430, - 1910 + 820, + 1230, + 1640 ] }, { @@ -922,13 +924,14 @@ "colors": 4, "quota": 30, "introBalls": 9, - "pushSpeed": 140, + "pushSpeed": 84, "powerUpRate": 0.065, + "background": "zuma-background-1", "seed": 24757, "starScores": [ - 1140, - 1710, - 2280 + 970, + 1460, + 1940 ] }, { @@ -1281,13 +1284,14 @@ "colors": 4, "quota": 32, "introBalls": 10, - "pushSpeed": 140, + "pushSpeed": 84, "powerUpRate": 0.065, + "background": "zuma-background-2", "seed": 32676, "starScores": [ - 1220, - 1820, - 2430 + 1040, + 1550, + 2070 ] }, { @@ -1497,13 +1501,14 @@ "colors": 4, "quota": 38, "introBalls": 11, - "pushSpeed": 148, + "pushSpeed": 88, "powerUpRate": 0.06, + "background": "zuma-background-1", "seed": 40595, "starScores": [ - 1480, - 2210, - 2950 + 1250, + 1870, + 2490 ] }, { @@ -1724,13 +1729,14 @@ "colors": 4, "quota": 34, "introBalls": 10, - "pushSpeed": 148, + "pushSpeed": 88, "powerUpRate": 0.06, + "background": "zuma-background-2", "seed": 48514, "starScores": [ - 1320, - 1980, - 2640 + 1120, + 1670, + 2230 ] }, { @@ -2103,13 +2109,14 @@ "colors": 4, "quota": 26, "introBalls": 8, - "pushSpeed": 180, + "pushSpeed": 108, "powerUpRate": 0.06, + "background": "zuma-background-1", "seed": 56433, "starScores": [ - 1090, - 1640, - 2180 + 910, + 1360, + 1810 ] }, { @@ -2263,13 +2270,14 @@ "colors": 5, "quota": 30, "introBalls": 9, - "pushSpeed": 160, + "pushSpeed": 96, "powerUpRate": 0.055, + "background": "zuma-background-2", "seed": 64352, "starScores": [ - 1200, - 1800, - 2400 + 1010, + 1520, + 2020 ] }, { @@ -2622,13 +2630,14 @@ "colors": 5, "quota": 36, "introBalls": 11, - "pushSpeed": 160, + "pushSpeed": 96, "powerUpRate": 0.055, + "background": "zuma-background-1", "seed": 72271, "starScores": [ - 1440, - 2160, - 2880 + 1210, + 1820, + 2420 ] }, { @@ -3022,13 +3031,14 @@ "colors": 5, "quota": 34, "introBalls": 10, - "pushSpeed": 172, + "pushSpeed": 104, "powerUpRate": 0.055, + "background": "zuma-background-2", "seed": 80190, "starScores": [ - 1400, - 2100, - 2800 + 1170, + 1760, + 2340 ] }, { @@ -3238,13 +3248,14 @@ "colors": 5, "quota": 42, "introBalls": 12, - "pushSpeed": 172, + "pushSpeed": 104, "powerUpRate": 0.05, + "background": "zuma-background-1", "seed": 88109, "starScores": [ - 1730, - 2600, - 3460 + 1450, + 2170, + 2890 ] }, { @@ -3465,13 +3476,14 @@ "colors": 5, "quota": 38, "introBalls": 11, - "pushSpeed": 180, + "pushSpeed": 108, "powerUpRate": 0.05, + "background": "zuma-background-2", "seed": 96028, "starScores": [ - 1600, - 2390, - 3190 + 1320, + 1980, + 2640 ] }, { @@ -3824,13 +3836,14 @@ "colors": 5, "quota": 34, "introBalls": 10, - "pushSpeed": 192, + "pushSpeed": 116, "powerUpRate": 0.05, + "background": "zuma-background-1", "seed": 103947, "starScores": [ - 1470, - 2210, - 2940 + 1210, + 1820, + 2420 ] }, { @@ -4044,13 +4057,14 @@ "colors": 5, "quota": 46, "introBalls": 13, - "pushSpeed": 192, + "pushSpeed": 116, "powerUpRate": 0.05, + "background": "zuma-background-2", "seed": 111866, "starScores": [ - 1990, - 2980, - 3970 + 1640, + 2460, + 3280 ] }, { @@ -4199,13 +4213,14 @@ "colors": 6, "quota": 32, "introBalls": 9, - "pushSpeed": 204, + "pushSpeed": 124, "powerUpRate": 0.05, + "background": "zuma-background-1", "seed": 119785, "starScores": [ - 1420, - 2130, - 2840 + 1170, + 1750, + 2330 ] }, { @@ -4599,13 +4614,14 @@ "colors": 6, "quota": 38, "introBalls": 11, - "pushSpeed": 212, + "pushSpeed": 128, "powerUpRate": 0.05, + "background": "zuma-background-2", "seed": 127704, "starScores": [ - 1720, - 2580, - 3440 + 1400, + 2100, + 2800 ] }, { @@ -4958,13 +4974,14 @@ "colors": 6, "quota": 34, "introBalls": 10, - "pushSpeed": 200, + "pushSpeed": 120, "powerUpRate": 0.045, + "background": "zuma-background-1", "seed": 135623, "starScores": [ - 1500, - 2240, - 2990 + 1230, + 1840, + 2450 ] }, { @@ -5185,13 +5202,14 @@ "colors": 6, "quota": 40, "introBalls": 11, - "pushSpeed": 200, + "pushSpeed": 120, "powerUpRate": 0.045, + "background": "zuma-background-2", "seed": 143542, "starScores": [ - 1760, - 2640, - 3520 + 1440, + 2160, + 2880 ] }, { @@ -5396,13 +5414,14 @@ "colors": 6, "quota": 50, "introBalls": 14, - "pushSpeed": 244, + "pushSpeed": 148, "powerUpRate": 0.045, + "background": "zuma-background-1", "seed": 151461, "starScores": [ - 2420, - 3630, - 4840 + 1940, + 2910, + 3880 ] }, { @@ -5607,13 +5626,14 @@ "colors": 6, "quota": 52, "introBalls": 14, - "pushSpeed": 256, + "pushSpeed": 152, "powerUpRate": 0.045, + "background": "zuma-background-2", "seed": 159380, "starScores": [ - 2580, - 3870, - 5160 + 2040, + 3060, + 4080 ] } ] diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index 65270b9..e037b8c 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -315,6 +315,10 @@ export const MANIFEST = { ], zuma: [ image('zuma-menu-bg', 'assets/images/zuma/background-menu.png'), + // In-level backgrounds, alternated per level by ZumaLogic's `background` + // field (see tools/genZuma.js) — more can be dropped in later. + image('zuma-background-1', 'assets/images/zuma/background-01.png'), + image('zuma-background-2', 'assets/images/zuma/background-02.png'), // frame 0 = the whole stone frog, frame 1 = the same disc with the mouth // slot cut out; the ready marble is drawn between them. sheet('zuma-frog', 'assets/images/zuma/frog.png', 200, 200), @@ -328,6 +332,7 @@ export const MANIFEST = { { type: 'audio', key: 'sfx-zuma-start', path: 'assets/fx/zuma-start.mp3' }, { type: 'audio', key: 'sfx-zuma-explode', path: 'assets/fx/zuma-explode.mp3' }, { type: 'audio', key: 'sfx-zuma-win', path: 'assets/fx/zuma-win.mp3' }, + { type: 'audio', key: 'sfx-zuma-lose', path: 'assets/fx/zuma-lose.mp3' }, ], '2048': [ // hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so diff --git a/src/games/zuma/ZumaGame.js b/src/games/zuma/ZumaGame.js index 0a9402d..8b6a2fa 100644 --- a/src/games/zuma/ZumaGame.js +++ b/src/games/zuma/ZumaGame.js @@ -561,6 +561,7 @@ export default class ZumaGame extends Phaser.Scene { this.aimAngle = -Math.PI / 2; this.clearLayer(); + this.drawBackground(); this.drawPath(); this.drawPortals(); this.drawPit(); @@ -587,6 +588,26 @@ export default class ZumaGame extends Phaser.Scene { sfx.play(); } + // Same duck/play/resume trick as playStartFanfare(), for the chain going + // over the edge instead of clearing the level. + playLoseFanfare() { + this.music?.pause(); + const sfx = this.sound.add('sfx-zuma-lose'); + sfx.once(Phaser.Sound.Events.COMPLETE, () => this.music?.resume()); + sfx.play(); + } + + // Per-level art (def.background, see tools/genZuma.js) over the solid BG + // rectangle laid down once in create() — that rectangle is the fallback for + // any level whose background key isn't painted yet. + drawBackground() { + const key = this.levelDef.background; + if (!key || !this.textures.exists(key)) return; + const img = this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, key) + .setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(D.bg); + this.layer.add(img); + } + drawPath() { const path = this.state.path; const tunnels = this.state.tunnels; @@ -1050,7 +1071,7 @@ export default class ZumaGame extends Phaser.Scene { onLost() { this.overlayUp = true; - playSound(this, SFX.CASINO_LOSE); + this.playLoseFanfare(); this.laserGfx?.clear(); if (!this.testLevel) { @@ -1160,6 +1181,7 @@ export default class ZumaGame extends Phaser.Scene { const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2; const banner = this.add.text(cx, cy - 60, 'ZUMA!', { fontFamily: 'Righteous', fontSize: '140px', color: COLORS.goldHex, + stroke: '#000000', strokeThickness: 8, }).setOrigin(0.5).setScale(0.2).setDepth(D.fx); this.layer.add(banner); this.tweens.add({ targets: banner, scale: 1, duration: 450, ease: 'Back.easeOut' }); diff --git a/tools/genZuma.js b/tools/genZuma.js index c70e6e6..cc533c1 100644 --- a/tools/genZuma.js +++ b/tools/genZuma.js @@ -178,28 +178,31 @@ const SHAPES = { sCurve, horseshoe, spiral, zigzag, doubleLoop, figureEight }; // never on zigzag (already only ~57% shootable), and never long enough that the // frog runs out of targets while it waits for the chain to surface. +// pushSpeed values are ~60% of their original tuning (a 2026-07-28 across-the- +// board slowdown) — starScores() derives its thresholds from pushSpeed, so +// star bands shifted down with it automatically on regen. const TABLE = [ // level, name, shape, colors, quota, intro, push, powerUpRate, tunnels - [1, 'Riverbend', 'sCurve', 4, 22, 7, 100, 0.07], - [2, 'Temple Gate', 'horseshoe', 4, 26, 8, 128, 0.07], - [3, 'Twin Pools', 'doubleLoop', 4, 30, 9, 140, 0.065], - [4, 'Switchbacks', 'zigzag', 4, 32, 10, 140, 0.065], - [5, 'Serpent Coil', 'spiral', 4, 38, 11, 148, 0.06, [[0.36, 0.45]]], - [6, 'Crossroads', 'figureEight', 4, 34, 10, 148, 0.06], - [7, 'Rapids', 'sCurve', 4, 26, 8, 180, 0.06], - [8, 'Sun Court', 'horseshoe', 5, 30, 9, 160, 0.055, [[0.34, 0.44]]], - [9, 'Thunder Steps', 'zigzag', 5, 36, 11, 160, 0.055], - [10, 'Twin Serpents', 'doubleLoop', 5, 34, 10, 172, 0.055, [[0.40, 0.50]]], - [11, 'Deep Coil', 'spiral', 5, 42, 12, 172, 0.05, [[0.30, 0.40]]], - [12, 'Tangled Path', 'figureEight', 5, 38, 11, 180, 0.05], - [13, 'Lightning Run', 'zigzag', 5, 34, 10, 192, 0.05], - [14, 'Whirlpool', 'spiral', 5, 46, 13, 192, 0.05, [[0.26, 0.36], [0.60, 0.70]]], - [15, 'Obsidian Gate', 'horseshoe', 6, 32, 9, 204, 0.05], - [16, 'Twin Tempests', 'doubleLoop', 6, 38, 11, 212, 0.05, [[0.38, 0.46]]], - [17, 'Stormsteps', 'zigzag', 6, 34, 10, 200, 0.045], - [18, 'Maelstrom Cross', 'figureEight', 6, 40, 11, 200, 0.045], - [19, 'Abyss Coil', 'spiral', 6, 50, 14, 244, 0.045], - [20, 'The Final Coil', 'spiral', 6, 52, 14, 256, 0.045], + [1, 'Riverbend', 'sCurve', 4, 22, 7, 60, 0.07], + [2, 'Temple Gate', 'horseshoe', 4, 26, 8, 76, 0.07], + [3, 'Twin Pools', 'doubleLoop', 4, 30, 9, 84, 0.065], + [4, 'Switchbacks', 'zigzag', 4, 32, 10, 84, 0.065], + [5, 'Serpent Coil', 'spiral', 4, 38, 11, 88, 0.06, [[0.36, 0.45]]], + [6, 'Crossroads', 'figureEight', 4, 34, 10, 88, 0.06], + [7, 'Rapids', 'sCurve', 4, 26, 8, 108, 0.06], + [8, 'Sun Court', 'horseshoe', 5, 30, 9, 96, 0.055, [[0.34, 0.44]]], + [9, 'Thunder Steps', 'zigzag', 5, 36, 11, 96, 0.055], + [10, 'Twin Serpents', 'doubleLoop', 5, 34, 10, 104, 0.055, [[0.40, 0.50]]], + [11, 'Deep Coil', 'spiral', 5, 42, 12, 104, 0.05, [[0.30, 0.40]]], + [12, 'Tangled Path', 'figureEight', 5, 38, 11, 108, 0.05], + [13, 'Lightning Run', 'zigzag', 5, 34, 10, 116, 0.05], + [14, 'Whirlpool', 'spiral', 5, 46, 13, 116, 0.05, [[0.26, 0.36], [0.60, 0.70]]], + [15, 'Obsidian Gate', 'horseshoe', 6, 32, 9, 124, 0.05], + [16, 'Twin Tempests', 'doubleLoop', 6, 38, 11, 128, 0.05, [[0.38, 0.46]]], + [17, 'Stormsteps', 'zigzag', 6, 34, 10, 120, 0.045], + [18, 'Maelstrom Cross', 'figureEight', 6, 40, 11, 120, 0.045], + [19, 'Abyss Coil', 'spiral', 6, 50, 14, 148, 0.045], + [20, 'The Final Coil', 'spiral', 6, 52, 14, 152, 0.045], ]; // Calibrated against the headless aimbot in verifyZuma.js, which takes the best @@ -214,6 +217,10 @@ function starScores(quota, pushSpeed) { // ── Build & write ──────────────────────────────────────────────────────────── +// In-level background art (see data/assetManifest.js), alternated one per +// level while there are only two — drop more keys in here as they're painted. +const BACKGROUNDS = ['zuma-background-1', 'zuma-background-2']; + const levels = []; let bad = 0; for (const [level, name, shape, colors, quota, introBalls, pushSpeed, powerUpRate, tunFrac] of TABLE) { @@ -222,6 +229,7 @@ for (const [level, name, shape, colors, quota, introBalls, pushSpeed, powerUpRat const tunnels = (tunFrac ?? []).map(([a, b]) => [Math.round(len * a), Math.round(len * b)]); const def = { level, name, shape, points, frog, tunnels, colors, quota, introBalls, pushSpeed, powerUpRate, + background: BACKGROUNDS[(level - 1) % BACKGROUNDS.length], seed: 1000 + level * 7919, starScores: starScores(quota, pushSpeed), };