feat: overhaul Pudding Monsters scoring and add opponent videos
- Replace star collection with target-square coverage for Pudding Monsters - New 3-star medal system: min(targets covered, par-based efficiency) - Add "Reset Progress" buttons to Pudding Monsters and Rush Hour level select - Add reset API endpoint for puzzle progress - Add Beth and Blackwind opponents with idle/happy/upset animations - Update puddingmonsters.json levels with target squares instead of stars
This commit is contained in:
parent
ea44758f7d
commit
85f0079b2c
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 3.8 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -500,6 +500,18 @@
|
||||||
"steve-pick"
|
"steve-pick"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "beth",
|
||||||
|
"spriteIndex": 18,
|
||||||
|
"name": "Beth",
|
||||||
|
"bio": "I ain't seen you 'round these parts before."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "blackwind",
|
||||||
|
"spriteIndex": 19,
|
||||||
|
"name": "Blackwind",
|
||||||
|
"bio": "Aaaaaaargh! Me be playin' these games matey!"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"generatedAt": "2026-06-09T03:30:42.804Z",
|
"generatedAt": "2026-06-09T04:18:02.258Z",
|
||||||
"seed": 1592594996,
|
"seed": 1592594996,
|
||||||
"count": 40,
|
"count": 40,
|
||||||
"levels": [
|
"levels": [
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
4,
|
4,
|
||||||
2
|
2
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
0
|
0
|
||||||
|
|
@ -189,7 +189,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
3
|
3
|
||||||
|
|
@ -225,7 +225,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
3
|
3
|
||||||
|
|
@ -261,7 +261,7 @@
|
||||||
"rows": 5,
|
"rows": 5,
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
0
|
0
|
||||||
|
|
@ -306,7 +306,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
0
|
0
|
||||||
|
|
@ -351,7 +351,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
5,
|
5,
|
||||||
4
|
4
|
||||||
|
|
@ -396,7 +396,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
2,
|
2,
|
||||||
0
|
0
|
||||||
|
|
@ -441,7 +441,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
2,
|
2,
|
||||||
4
|
4
|
||||||
|
|
@ -486,7 +486,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
4,
|
4,
|
||||||
4
|
4
|
||||||
|
|
@ -531,7 +531,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
2,
|
2,
|
||||||
0
|
0
|
||||||
|
|
@ -576,7 +576,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
1
|
1
|
||||||
|
|
@ -621,7 +621,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
|
|
@ -670,17 +670,17 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
3
|
3
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
4
|
5
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
1,
|
2,
|
||||||
5
|
5
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
@ -723,7 +723,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
2,
|
2,
|
||||||
0
|
0
|
||||||
|
|
@ -776,7 +776,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
|
|
@ -829,7 +829,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
4
|
4
|
||||||
|
|
@ -882,7 +882,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
0
|
0
|
||||||
|
|
@ -935,7 +935,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
2,
|
2,
|
||||||
3
|
3
|
||||||
|
|
@ -988,7 +988,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
0
|
0
|
||||||
|
|
@ -1041,7 +1041,7 @@
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"spikes": [],
|
"spikes": [],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
5,
|
5,
|
||||||
0
|
0
|
||||||
|
|
@ -1103,7 +1103,7 @@
|
||||||
3
|
3
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
4,
|
4,
|
||||||
0
|
0
|
||||||
|
|
@ -1165,7 +1165,7 @@
|
||||||
4
|
4
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
4
|
4
|
||||||
|
|
@ -1227,7 +1227,7 @@
|
||||||
1
|
1
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
0
|
0
|
||||||
|
|
@ -1289,7 +1289,7 @@
|
||||||
6
|
6
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
5,
|
5,
|
||||||
3
|
3
|
||||||
|
|
@ -1351,7 +1351,7 @@
|
||||||
0
|
0
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
3
|
3
|
||||||
|
|
@ -1413,17 +1413,17 @@
|
||||||
3
|
3
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
6,
|
6,
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
6,
|
5,
|
||||||
2
|
3
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
5,
|
6,
|
||||||
3
|
3
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
@ -1475,7 +1475,7 @@
|
||||||
5
|
5
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
0
|
0
|
||||||
|
|
@ -1537,9 +1537,9 @@
|
||||||
3
|
3
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
2,
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
@ -1607,7 +1607,7 @@
|
||||||
6
|
6
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
2
|
2
|
||||||
|
|
@ -1618,7 +1618,7 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
4,
|
4,
|
||||||
4
|
5
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"monsters": [
|
"monsters": [
|
||||||
|
|
@ -1681,13 +1681,13 @@
|
||||||
4
|
4
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
|
||||||
4,
|
|
||||||
3
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
5,
|
5,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
3
|
3
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
@ -1755,7 +1755,7 @@
|
||||||
5
|
5
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
2,
|
2,
|
||||||
0
|
0
|
||||||
|
|
@ -1765,7 +1765,7 @@
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
2,
|
3,
|
||||||
2
|
2
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
@ -1829,13 +1829,13 @@
|
||||||
4
|
4
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
1,
|
1,
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
4,
|
3,
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
@ -1903,13 +1903,13 @@
|
||||||
3
|
3
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
2,
|
0,
|
||||||
4
|
4
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
1,
|
0,
|
||||||
5
|
5
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
@ -1977,13 +1977,13 @@
|
||||||
1
|
1
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
4
|
3
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
2,
|
1,
|
||||||
4
|
4
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
@ -2051,7 +2051,7 @@
|
||||||
2
|
2
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
|
|
@ -2061,8 +2061,8 @@
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
3,
|
0,
|
||||||
0
|
1
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"monsters": [
|
"monsters": [
|
||||||
|
|
@ -2125,7 +2125,7 @@
|
||||||
3
|
3
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"stars": [
|
"targets": [
|
||||||
[
|
[
|
||||||
3,
|
3,
|
||||||
0
|
0
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||||
import { api } from '../../services/api.js';
|
import { api } from '../../services/api.js';
|
||||||
import {
|
import {
|
||||||
DIRS, newState, cloneState, slide, computeSlide, solve, blobAt, repCell, starsCollected,
|
DIRS, newState, cloneState, slide, computeSlide, solve, blobAt, repCell, targetsCovered,
|
||||||
} from './PuddingMonstersLogic.js';
|
} from './PuddingMonstersLogic.js';
|
||||||
|
|
||||||
const BG = 0x161226;
|
const BG = 0x161226;
|
||||||
|
|
@ -16,12 +16,12 @@ const WALL = 0x0e0a1a;
|
||||||
const WALL_HI = 0x3a2f56;
|
const WALL_HI = 0x3a2f56;
|
||||||
const SPIKE_BG = 0x3a1320;
|
const SPIKE_BG = 0x3a1320;
|
||||||
const SPIKE = 0xe0506b;
|
const SPIKE = 0xe0506b;
|
||||||
const STAR_OFF = 0x6b5a8a;
|
const TARGET = 0xffd54a;
|
||||||
const STAR_ON = 0xffd54a;
|
const TARGET_LN = 0xffec99;
|
||||||
|
|
||||||
const PALETTE = [0xff7eb6, 0x7ed957, 0x5bc0ff, 0xffd166, 0xc792ff, 0xff9e64, 0x4ecdc4, 0xf6e58d];
|
const PALETTE = [0xff7eb6, 0x7ed957, 0x5bc0ff, 0xffd166, 0xc792ff, 0xff9e64, 0x4ecdc4, 0xf6e58d];
|
||||||
|
|
||||||
const D = { frame: -1, floor: 0, deco: 1, star: 2, monster: 10, anim: 12, ui: 30, overlay: 60, overlayUI: 62 };
|
const D = { frame: -1, floor: 0, deco: 1, monster: 10, target: 11, anim: 12, ui: 30, overlay: 60, overlayUI: 62 };
|
||||||
|
|
||||||
export default class PuddingMonstersGame extends Phaser.Scene {
|
export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
constructor() { super('PuddingMonstersGame'); }
|
constructor() { super('PuddingMonstersGame'); }
|
||||||
|
|
@ -79,7 +79,7 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
clearLayer() {
|
clearLayer() {
|
||||||
this.layer.removeAll(true);
|
this.layer.removeAll(true);
|
||||||
this.monsterGfx = null;
|
this.monsterGfx = null;
|
||||||
this.starGfx = null;
|
this.targetGfx = null;
|
||||||
this.animGfx = null;
|
this.animGfx = null;
|
||||||
this.undoBtn = null;
|
this.undoBtn = null;
|
||||||
this.movesText = null;
|
this.movesText = null;
|
||||||
|
|
@ -96,6 +96,11 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
} catch (_) { /* ignore */ }
|
} catch (_) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3-star medal by efficiency: full marks at par, one star off per move over.
|
||||||
|
medalStars(moves, par) {
|
||||||
|
return Math.max(0, 3 - Math.max(0, moves - par));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Level select ────────────────────────────────────────────────────────────
|
// ── Level select ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
showLevelSelect() {
|
showLevelSelect() {
|
||||||
|
|
@ -109,7 +114,7 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
const title = this.add.text(cx, 84, 'JELL-O MONSTERS', {
|
const title = this.add.text(cx, 84, 'JELL-O MONSTERS', {
|
||||||
fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex,
|
fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex,
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
const sub = this.add.text(cx, 140, 'Flick the jellies so they slide and stick — merge them all into one. Grab the 3 stars!', {
|
const sub = this.add.text(cx, 140, 'Flick the jellies to slide & stick. Merge them into one — finish on the yellow squares, in par, for ★★★.', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
|
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this.layer.add([title, sub]);
|
this.layer.add([title, sub]);
|
||||||
|
|
@ -183,7 +188,9 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
{ width: 300, height: 58, fontSize: 24 });
|
{ width: 300, height: 58, fontSize: 24 });
|
||||||
const back = new Button(this, cx + 170, GAME_HEIGHT - 76, 'Back', () => this.scene.start('GameMenu'),
|
const back = new Button(this, cx + 170, GAME_HEIGHT - 76, 'Back', () => this.scene.start('GameMenu'),
|
||||||
{ variant: 'ghost', width: 180, height: 58, fontSize: 24 });
|
{ variant: 'ghost', width: 180, height: 58, fontSize: 24 });
|
||||||
this.layer.add([resume, back]);
|
const reset = new Button(this, 210, GAME_HEIGHT - 76, 'Reset Progress', () => this.confirmResetProgress(),
|
||||||
|
{ variant: 'ghost', width: 260, height: 58, fontSize: 22, textColor: COLORS.dangerHex });
|
||||||
|
this.layer.add([resume, back, reset]);
|
||||||
|
|
||||||
if (!this.canPersist) {
|
if (!this.canPersist) {
|
||||||
const note = this.add.text(cx, GAME_HEIGHT - 26, 'Sign in to save your progress across devices.', {
|
const note = this.add.text(cx, GAME_HEIGHT - 26, 'Sign in to save your progress across devices.', {
|
||||||
|
|
@ -193,6 +200,36 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmResetProgress() {
|
||||||
|
const cx = GAME_WIDTH / 2;
|
||||||
|
const cy = GAME_HEIGHT / 2;
|
||||||
|
const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setInteractive();
|
||||||
|
const panel = this.add.graphics();
|
||||||
|
panel.fillStyle(COLORS.panel, 0.98);
|
||||||
|
panel.fillRoundedRect(cx - 320, cy - 160, 640, 320, 20);
|
||||||
|
panel.lineStyle(3, COLORS.danger, 1);
|
||||||
|
panel.strokeRoundedRect(cx - 320, cy - 160, 640, 320, 20);
|
||||||
|
const title = this.add.text(cx, cy - 92, 'Reset Progress?', {
|
||||||
|
fontFamily: 'Righteous', fontSize: '52px', color: COLORS.dangerHex,
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
const msg = this.add.text(cx, cy - 14,
|
||||||
|
'This clears every cleared level and your star\nmedals, back to Level 1. This cannot be undone.', {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex, align: 'center', lineSpacing: 6,
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
const yes = new Button(this, cx - 150, cy + 88, 'Reset', () => this.doResetProgress(),
|
||||||
|
{ width: 250, height: 58, fontSize: 24, textColor: COLORS.dangerHex });
|
||||||
|
const no = new Button(this, cx + 150, cy + 88, 'Cancel', () => this.showLevelSelect(),
|
||||||
|
{ variant: 'ghost', width: 250, height: 58, fontSize: 24 });
|
||||||
|
this.layer.add([dim, panel, title, msg, yes, no]);
|
||||||
|
}
|
||||||
|
|
||||||
|
doResetProgress() {
|
||||||
|
api.post('/puzzles/puddingmonsters/reset').catch(() => { /* best effort */ });
|
||||||
|
this.levelsCompleted = 0;
|
||||||
|
try { this.bank.forEach((p) => localStorage.removeItem(`pm-stars-${p.level}`)); } catch (_) { /* ignore */ }
|
||||||
|
this.showLevelSelect();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Play a level ──────────────────────────────────────────────────────────────
|
// ── Play a level ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
playLevel(level) {
|
playLevel(level) {
|
||||||
|
|
@ -213,10 +250,10 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
this.clearLayer();
|
this.clearLayer();
|
||||||
this.computeLayout();
|
this.computeLayout();
|
||||||
this.buildBoard();
|
this.buildBoard();
|
||||||
this.starGfx = this.add.graphics().setDepth(D.star); this.layer.add(this.starGfx);
|
|
||||||
this.monsterGfx = this.add.graphics().setDepth(D.monster); this.layer.add(this.monsterGfx);
|
this.monsterGfx = this.add.graphics().setDepth(D.monster); this.layer.add(this.monsterGfx);
|
||||||
this.drawStars();
|
this.targetGfx = this.add.graphics().setDepth(D.target); this.layer.add(this.targetGfx);
|
||||||
this.drawAllMonsters();
|
this.drawAllMonsters();
|
||||||
|
this.drawTargets();
|
||||||
this.drawHud();
|
this.drawHud();
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +304,10 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
floor.strokeRoundedRect(this.cellLeft(x) + 2, this.cellTop(y) + 2, this.cell - 4, this.cell - 4, 8);
|
floor.strokeRoundedRect(this.cellLeft(x) + 2, this.cellTop(y) + 2, this.cell - 4, this.cell - 4, 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const [tx, ty] of (this.levelDef.targets ?? [])) {
|
||||||
|
floor.fillStyle(TARGET, 0.30);
|
||||||
|
floor.fillRoundedRect(this.cellLeft(tx) + 2, this.cellTop(ty) + 2, this.cell - 4, this.cell - 4, 8);
|
||||||
|
}
|
||||||
this.layer.add(floor);
|
this.layer.add(floor);
|
||||||
|
|
||||||
const deco = this.add.graphics().setDepth(D.deco);
|
const deco = this.add.graphics().setDepth(D.deco);
|
||||||
|
|
@ -293,44 +334,31 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
this.layer.add(deco);
|
this.layer.add(deco);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawStars() {
|
|
||||||
const g = this.starGfx;
|
|
||||||
g.clear();
|
|
||||||
for (const [sx, sy] of this.state.stars) {
|
|
||||||
const on = this.state.collected.has(`${sx},${sy}`);
|
|
||||||
const cx = this.cellCx(sx), cy = this.cellCy(sy);
|
|
||||||
if (on) {
|
|
||||||
g.fillStyle(STAR_ON, 0.22);
|
|
||||||
g.fillCircle(cx, cy, this.cell * 0.42);
|
|
||||||
}
|
|
||||||
this.drawStarShape(g, cx, cy, this.cell * 0.26, on);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawStarShape(g, cx, cy, r, filled) {
|
|
||||||
const pts = [];
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const ang = (Math.PI / 180) * (-90 + i * 36);
|
|
||||||
const rad = i % 2 === 0 ? r : r * 0.45;
|
|
||||||
pts.push(cx + rad * Math.cos(ang), cy + rad * Math.sin(ang));
|
|
||||||
}
|
|
||||||
if (filled) {
|
|
||||||
g.fillStyle(STAR_ON, 1);
|
|
||||||
g.beginPath(); g.moveTo(pts[0], pts[1]);
|
|
||||||
for (let i = 2; i < pts.length; i += 2) g.lineTo(pts[i], pts[i + 1]);
|
|
||||||
g.closePath(); g.fillPath();
|
|
||||||
}
|
|
||||||
g.lineStyle(3, filled ? 0xffec99 : STAR_OFF, 1);
|
|
||||||
g.beginPath(); g.moveTo(pts[0], pts[1]);
|
|
||||||
for (let i = 2; i < pts.length; i += 2) g.lineTo(pts[i], pts[i + 1]);
|
|
||||||
g.closePath(); g.strokePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
colorFor(blob) {
|
colorFor(blob) {
|
||||||
const [x, y] = repCell(blob);
|
const [x, y] = repCell(blob);
|
||||||
return PALETTE[(x * 7 + y * 13) % PALETTE.length];
|
return PALETTE[(x * 7 + y * 13) % PALETTE.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yellow target-square outlines, drawn above the monsters so coverage stays
|
||||||
|
// visible (white + thick when a monster is currently sitting on the square).
|
||||||
|
drawTargets() {
|
||||||
|
const g = this.targetGfx;
|
||||||
|
if (!g) return;
|
||||||
|
g.clear();
|
||||||
|
for (const [tx, ty] of (this.levelDef.targets ?? [])) {
|
||||||
|
const covered = blobAt(this.state, tx, ty) >= 0;
|
||||||
|
const L = this.cellLeft(tx), T = this.cellTop(ty), c = this.cell;
|
||||||
|
g.lineStyle(covered ? 5 : 3, covered ? 0xffffff : TARGET_LN, covered ? 1 : 0.9);
|
||||||
|
g.strokeRoundedRect(L + 5, T + 5, c - 10, c - 10, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live star projection: you need the yellow squares covered AND par moves.
|
||||||
|
liveStars() {
|
||||||
|
const covered = targetsCovered(this.state, this.levelDef.targets);
|
||||||
|
return Math.min(covered, this.medalStars(this.moves, this.par));
|
||||||
|
}
|
||||||
|
|
||||||
drawAllMonsters(excludeIdx = -1) {
|
drawAllMonsters(excludeIdx = -1) {
|
||||||
const g = this.monsterGfx;
|
const g = this.monsterGfx;
|
||||||
g.clear();
|
g.clear();
|
||||||
|
|
@ -410,7 +438,7 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
this.undoBtn = undo;
|
this.undoBtn = undo;
|
||||||
this.layer.add([undo, reset, hint, levels]);
|
this.layer.add([undo, reset, hint, levels]);
|
||||||
|
|
||||||
const tip = this.add.text(BTN_X, y + BTN_H + 26, 'Drag a monster to\nslide it. Arrow keys\nflick the selected one.', {
|
const tip = this.add.text(BTN_X, y + BTN_H + 26, 'Drag a monster to slide it\n(or arrow keys). Finish on the\nyellow squares, in par, for ★★★.', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex, align: 'center',
|
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex, align: 'center',
|
||||||
}).setOrigin(0.5, 0).setDepth(D.ui);
|
}).setOrigin(0.5, 0).setDepth(D.ui);
|
||||||
this.layer.add(tip);
|
this.layer.add(tip);
|
||||||
|
|
@ -418,7 +446,7 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
|
|
||||||
updateHud() {
|
updateHud() {
|
||||||
if (this.movesText) this.movesText.setText(`Moves: ${this.moves} Par: ${this.par}`);
|
if (this.movesText) this.movesText.setText(`Moves: ${this.moves} Par: ${this.par}`);
|
||||||
if (this.starsText) this.starsText.setText(`★ ${starsCollected(this.state)}/3`);
|
if (this.starsText) this.starsText.setText(`★ ${this.liveStars()}/3`);
|
||||||
if (this.undoBtn) this.undoBtn.setEnabled(this.undoStack.length > 0);
|
if (this.undoBtn) this.undoBtn.setEnabled(this.undoStack.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,7 +528,7 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
// keep the moved blob selected at its new resting position
|
// keep the moved blob selected at its new resting position
|
||||||
this.selectedCell = { x: movingCells[0][0] + dx * steps, y: movingCells[0][1] + dy * steps };
|
this.selectedCell = { x: movingCells[0][0] + dx * steps, y: movingCells[0][1] + dy * steps };
|
||||||
this.drawAllMonsters();
|
this.drawAllMonsters();
|
||||||
this.drawStars();
|
this.drawTargets();
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
if (res.merged) playSound(this, SFX.CARD_PLACE);
|
if (res.merged) playSound(this, SFX.CARD_PLACE);
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
|
|
@ -535,7 +563,7 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
this.moves++; // an undo still counts as a move (matches Rush Hour)
|
this.moves++; // an undo still counts as a move (matches Rush Hour)
|
||||||
this.selectedCell = null;
|
this.selectedCell = null;
|
||||||
this.drawAllMonsters();
|
this.drawAllMonsters();
|
||||||
this.drawStars();
|
this.drawTargets();
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
playSound(this, SFX.PIECE_CLICK);
|
playSound(this, SFX.PIECE_CLICK);
|
||||||
}
|
}
|
||||||
|
|
@ -547,7 +575,7 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
this.moves = 0;
|
this.moves = 0;
|
||||||
this.selectedCell = null;
|
this.selectedCell = null;
|
||||||
this.drawAllMonsters();
|
this.drawAllMonsters();
|
||||||
this.drawStars();
|
this.drawTargets();
|
||||||
this.updateHud();
|
this.updateHud();
|
||||||
playSound(this, SFX.CARD_SHUFFLE);
|
playSound(this, SFX.CARD_SHUFFLE);
|
||||||
}
|
}
|
||||||
|
|
@ -605,7 +633,8 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
|
|
||||||
onSolved() {
|
onSolved() {
|
||||||
this.overlayUp = true;
|
this.overlayUp = true;
|
||||||
const stars = starsCollected(this.state);
|
const covered = targetsCovered(this.state, this.levelDef.targets);
|
||||||
|
const stars = Math.min(covered, this.medalStars(this.moves, this.par));
|
||||||
this.saveStars(this.level, stars);
|
this.saveStars(this.level, stars);
|
||||||
|
|
||||||
if (this.level > this.levelsCompleted) this.levelsCompleted = this.level;
|
if (this.level > this.levelsCompleted) this.levelsCompleted = this.level;
|
||||||
|
|
@ -635,13 +664,21 @@ export default class PuddingMonstersGame extends Phaser.Scene {
|
||||||
const starRow = this.add.text(cx, cy - 64, '★★★'.slice(0, stars) + '☆☆☆'.slice(0, 3 - stars), {
|
const starRow = this.add.text(cx, cy - 64, '★★★'.slice(0, stars) + '☆☆☆'.slice(0, 3 - stars), {
|
||||||
fontFamily: 'serif', fontSize: '56px', color: '#ffd54a',
|
fontFamily: 'serif', fontSize: '56px', color: '#ffd54a',
|
||||||
}).setOrigin(0.5).setDepth(D.overlayUI);
|
}).setOrigin(0.5).setDepth(D.overlayUI);
|
||||||
const beatPar = this.moves <= this.par;
|
|
||||||
const stat = this.add.text(cx, cy - 4,
|
const stat = this.add.text(cx, cy - 4,
|
||||||
`Level ${this.level} cleared in ${this.moves} moves (par ${this.par})${beatPar ? ' ★ par or better!' : ''}`, {
|
`${this.moves} moves (par ${this.par}) • ${covered} / 3 yellow squares covered`, {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex,
|
fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(D.overlayUI);
|
}).setOrigin(0.5).setDepth(D.overlayUI);
|
||||||
this.layer.add([title, starRow, stat]);
|
this.layer.add([title, starRow, stat]);
|
||||||
|
|
||||||
|
if (stars < 3) {
|
||||||
|
const hint = this.add.text(cx, cy + 34,
|
||||||
|
covered < 3 ? 'Finish with your blob on all 3 yellow squares for more stars.'
|
||||||
|
: 'Solve in par or fewer moves to keep all your stars.', {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
|
||||||
|
}).setOrigin(0.5).setDepth(D.overlayUI);
|
||||||
|
this.layer.add(hint);
|
||||||
|
}
|
||||||
|
|
||||||
const hasNext = this.level < this.bank.length;
|
const hasNext = this.level < this.bank.length;
|
||||||
const btns = [];
|
const btns = [];
|
||||||
if (hasNext) {
|
if (hasNext) {
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,14 @@ export function starsCollected(state) {
|
||||||
return state.collected.size;
|
return state.collected.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How many of the given target cells are currently covered by a monster.
|
||||||
|
// Stars judge this at the final (won) configuration.
|
||||||
|
export function targetsCovered(state, targets) {
|
||||||
|
let n = 0;
|
||||||
|
for (const [tx, ty] of targets ?? []) if (occupiedAt(state, tx, ty)) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
// How far blob `idx` can slide in `dir`, and whether that path crosses a spike.
|
// How far blob `idx` can slide in `dir`, and whether that path crosses a spike.
|
||||||
// Pure (does not mutate). { maxSteps, deathStep } — deathStep>0 means fatal.
|
// Pure (does not mutate). { maxSteps, deathStep } — deathStep>0 means fatal.
|
||||||
export function computeSlide(state, idx, dir) {
|
export function computeSlide(state, idx, dir) {
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,9 @@ export default class RushHourGame extends Phaser.Scene {
|
||||||
{ width: 280, height: 58, fontSize: 24 });
|
{ width: 280, height: 58, fontSize: 24 });
|
||||||
const back = new Button(this, cx + 170, GAME_HEIGHT - 78, 'Back', () => this.scene.start('GameMenu'),
|
const back = new Button(this, cx + 170, GAME_HEIGHT - 78, 'Back', () => this.scene.start('GameMenu'),
|
||||||
{ variant: 'ghost', width: 180, height: 58, fontSize: 24 });
|
{ variant: 'ghost', width: 180, height: 58, fontSize: 24 });
|
||||||
this.layer.add([resume, back]);
|
const reset = new Button(this, 210, GAME_HEIGHT - 78, 'Reset Progress', () => this.confirmResetProgress(),
|
||||||
|
{ variant: 'ghost', width: 260, height: 58, fontSize: 22, textColor: COLORS.dangerHex });
|
||||||
|
this.layer.add([resume, back, reset]);
|
||||||
|
|
||||||
if (!this.canPersist) {
|
if (!this.canPersist) {
|
||||||
const note = this.add.text(cx, GAME_HEIGHT - 28, 'Sign in to save your progress across devices.', {
|
const note = this.add.text(cx, GAME_HEIGHT - 28, 'Sign in to save your progress across devices.', {
|
||||||
|
|
@ -166,6 +168,35 @@ export default class RushHourGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmResetProgress() {
|
||||||
|
const cx = GAME_WIDTH / 2;
|
||||||
|
const cy = GAME_HEIGHT / 2;
|
||||||
|
const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setInteractive();
|
||||||
|
const panel = this.add.graphics();
|
||||||
|
panel.fillStyle(COLORS.panel, 0.98);
|
||||||
|
panel.fillRoundedRect(cx - 320, cy - 160, 640, 320, 20);
|
||||||
|
panel.lineStyle(3, COLORS.danger, 1);
|
||||||
|
panel.strokeRoundedRect(cx - 320, cy - 160, 640, 320, 20);
|
||||||
|
const title = this.add.text(cx, cy - 92, 'Reset Progress?', {
|
||||||
|
fontFamily: 'Righteous', fontSize: '52px', color: COLORS.dangerHex,
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
const msg = this.add.text(cx, cy - 14,
|
||||||
|
'This clears every level you have cleared and\nstarts you back at Level 1. This cannot be undone.', {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex, align: 'center', lineSpacing: 6,
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
const yes = new Button(this, cx - 150, cy + 88, 'Reset', () => this.doResetProgress(),
|
||||||
|
{ width: 250, height: 58, fontSize: 24, textColor: COLORS.dangerHex });
|
||||||
|
const no = new Button(this, cx + 150, cy + 88, 'Cancel', () => this.showLevelSelect(),
|
||||||
|
{ variant: 'ghost', width: 250, height: 58, fontSize: 24 });
|
||||||
|
this.layer.add([dim, panel, title, msg, yes, no]);
|
||||||
|
}
|
||||||
|
|
||||||
|
doResetProgress() {
|
||||||
|
api.post('/puzzles/rushhour/reset').catch(() => { /* best effort */ });
|
||||||
|
this.levelsCompleted = 0;
|
||||||
|
this.showLevelSelect();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Play a level ────────────────────────────────────────────────────────────
|
// ── Play a level ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
playLevel(level) {
|
playLevel(level) {
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,14 @@ router.post('/:slug/complete', requireAuth, (req, res) => {
|
||||||
res.json({ levelsCompleted: level });
|
res.json({ levelsCompleted: level });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/puzzles/:slug/reset
|
||||||
|
// Clears the signed-in user's progress for this puzzle (back to level 0).
|
||||||
|
router.post('/:slug/reset', requireAuth, (req, res) => {
|
||||||
|
const { slug } = req.params;
|
||||||
|
if (!getGame(slug)) return res.status(400).json({ error: 'Unknown game slug.' });
|
||||||
|
|
||||||
|
db.prepare('DELETE FROM puzzle_progress WHERE user_id = ? AND slug = ?').run(req.user.id, slug);
|
||||||
|
res.json({ levelsCompleted: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
//
|
//
|
||||||
// For each difficulty tier it random-fills a grid with walls, spikes and K
|
// For each difficulty tier it random-fills a grid with walls, spikes and K
|
||||||
// monsters, runs the BFS solver to (a) reject unsolvable/trivial layouts and
|
// monsters, runs the BFS solver to (a) reject unsolvable/trivial layouts and
|
||||||
// (b) label each survivor with its minimum flick count (par), then places 3
|
// (b) label each survivor with its minimum flick count (par), then marks 3 cells
|
||||||
// guaranteed-collectable stars on cells of that solution's final footprint and
|
// of that solution's final footprint as yellow target squares and writes ordered
|
||||||
// writes ordered levels to public/data/puddingmonsters.json.
|
// levels to public/data/puddingmonsters.json. Stars at play time require BOTH:
|
||||||
|
// the final merged blob covering the targets AND solving in par (min of the two
|
||||||
|
// medals) — so the on-par solution lands on all 3 targets and scores 3 stars.
|
||||||
//
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
// node server/scripts/genPuddingMonsters.js [seed] [outFile]
|
// node server/scripts/genPuddingMonsters.js [seed] [outFile]
|
||||||
|
|
@ -77,7 +79,7 @@ function randomLevel(tier) {
|
||||||
const monsters = placeCells(tier.monsters, tier.cols, tier.rows, taken);
|
const monsters = placeCells(tier.monsters, tier.cols, tier.rows, taken);
|
||||||
if (!monsters) return null;
|
if (!monsters) return null;
|
||||||
return {
|
return {
|
||||||
cols: tier.cols, rows: tier.rows, walls, spikes, monsters, stars: [],
|
cols: tier.cols, rows: tier.rows, walls, spikes, monsters,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,14 +88,12 @@ function canonKey(lvl) {
|
||||||
return `${lvl.cols}x${lvl.rows}|M:${s(lvl.monsters)}|W:${s(lvl.walls)}|X:${s(lvl.spikes)}`;
|
return `${lvl.cols}x${lvl.rows}|M:${s(lvl.monsters)}|W:${s(lvl.walls)}|X:${s(lvl.spikes)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick 3 spread-out, ideally non-starting footprint cells -> guaranteed stars.
|
// 3 spread-out cells of the solution's final footprint -> yellow target squares.
|
||||||
function chooseStars(footprint, monsters) {
|
// The footprint always has >= 3 cells (>= 3 monsters), so this yields 3 distinct.
|
||||||
const startSet = new Set(monsters.map(([x, y]) => keyOf(x, y)));
|
function chooseTargets(footprint) {
|
||||||
const nonStart = footprint.filter(([x, y]) => !startSet.has(keyOf(x, y)));
|
const sorted = footprint.slice().sort((a, b) => (a[1] - b[1]) || (a[0] - b[0]));
|
||||||
const pool = nonStart.length >= 3 ? nonStart : footprint;
|
const idx = [...new Set([0, Math.floor(sorted.length / 2), sorted.length - 1])];
|
||||||
const sorted = pool.slice().sort((a, b) => (a[1] - b[1]) || (a[0] - b[0]));
|
return idx.map((i) => [sorted[i][0], sorted[i][1]]);
|
||||||
const idx = [0, Math.floor(sorted.length / 2), sorted.length - 1];
|
|
||||||
return [...new Set(idx)].map((i) => [sorted[i][0], sorted[i][1]]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Generate pool ────────────────────────────────────────────────────────────
|
// ── Generate pool ────────────────────────────────────────────────────────────
|
||||||
|
|
@ -130,8 +130,8 @@ while (attempts < MAX_ATTEMPTS && !tiersFull()) {
|
||||||
if (res.moves < Math.max(2, tier.minPar) || res.moves > tier.maxPar) continue;
|
if (res.moves < Math.max(2, tier.minPar) || res.moves > tier.maxPar) continue;
|
||||||
solved++;
|
solved++;
|
||||||
|
|
||||||
lvl.stars = chooseStars(res.footprint, lvl.monsters);
|
lvl.targets = chooseTargets(res.footprint);
|
||||||
if (lvl.stars.length !== 3) continue;
|
if (lvl.targets.length !== 3) continue;
|
||||||
lvl.par = res.moves;
|
lvl.par = res.moves;
|
||||||
buckets[ti].push(lvl);
|
buckets[ti].push(lvl);
|
||||||
|
|
||||||
|
|
@ -157,7 +157,7 @@ const levels = chosen.map((lvl, i) => ({
|
||||||
rows: lvl.rows,
|
rows: lvl.rows,
|
||||||
walls: lvl.walls,
|
walls: lvl.walls,
|
||||||
spikes: lvl.spikes,
|
spikes: lvl.spikes,
|
||||||
stars: lvl.stars,
|
targets: lvl.targets,
|
||||||
monsters: lvl.monsters,
|
monsters: lvl.monsters,
|
||||||
par: lvl.par,
|
par: lvl.par,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue