diff --git a/.claude/launch.json b/.claude/launch.json index 8b30f3b..e8f8d41 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -5,7 +5,8 @@ "name": "dev", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"], - "port": 5173 + "port": 5173, + "autoPort": true } ] } diff --git a/assets/process_sprites.py b/assets/process_sprites.py index 118a251..58ba216 100644 --- a/assets/process_sprites.py +++ b/assets/process_sprites.py @@ -20,7 +20,8 @@ OUT.mkdir(parents=True, exist_ok=True) TABLE = { "floor": ("floor_parquet.png", 96, True), "floor_carpet": ("floor_carpet.png", 96, True), - "floor_checker": ("floor_checker.png", 96, True), + # coarse checker: the original was fine-grained and read as noise under sprites + "floor_checker": ("floor_club_b.png", 96, True), "wall": ("wall_crate.png", 48, True), "wall_shelf": ("wall_shelf.png", 48, True), "wall_poster": ("wall_poster.png", 48, True), @@ -56,6 +57,45 @@ TABLE = { "port_selector": ("portrait_selector.png", 112, False), "port_promoter": ("portrait_promoter.png", 112, False), "port_soundtech": ("portrait_soundtech.png", 112, False), + # per-class player sprites (were generated but never wired — classes were tint-only) + "player_selector": ("player_selector.png", 48, False), + "player_promoter": ("player_promoter.png", 48, False), + "player_soundtech": ("player_soundtech.png", 48, False), + # idle villains + "scalper": ("enemy_scalper.png", 48, False), + "firemarshal": ("enemy_fire_marshal.png", 48, False), + # props + "speaker": ("prop_speaker.png", 44, False), + "strobe": ("prop_strobe.png", 40, False), + "turntable": ("prop_turntable.png", 44, False), + # --- batch 4: wall variants (3 per theme) kill the one-texture repetition --- + "wall_b": ("wall_crate_b.png", 48, True), + "wall_c": ("wall_crate_c.png", 48, True), + "wall_d": ("wall_crate_d.png", 48, True), + "wall_shelf_b": ("wall_shelf_b.png", 48, True), + "wall_shelf_c": ("wall_shelf_c.png", 48, True), + "wall_shelf_d": ("wall_shelf_d.png", 48, True), + "wall_poster_b": ("wall_poster_b.png", 48, True), + "wall_poster_c": ("wall_poster_c.png", 48, True), + "wall_poster_d": ("wall_poster_d.png", 48, True), + "wall_warehouse_b": ("wall_warehouse_b.png", 48, True), + "wall_warehouse_c": ("wall_warehouse_c.png", 48, True), + "wall_warehouse_d": ("wall_warehouse_d.png", 48, True), + "wall_festival_b": ("wall_festival_b.png", 48, True), + "wall_festival_c": ("wall_festival_c.png", 48, True), + "wall_festival_d": ("wall_festival_d.png", 48, True), + "wall_archive_b": ("wall_archive_b.png", 48, True), + "wall_archive_c": ("wall_archive_c.png", 48, True), + "wall_archive_d": ("wall_archive_d.png", 48, True), + # floor decals (scattered, non-colliding) + "decal_cables": ("decal_cables.png", 40, False), + "decal_cups": ("decal_cups.png", 36, False), + "decal_flyers": ("decal_flyers.png", 40, False), + "decal_confetti": ("decal_confetti.png", 44, False), + "decal_tape": ("decal_tape.png", 36, False), + "decal_sleeve": ("decal_sleeve.png", 40, False), + "decal_dust": ("decal_dust.png", 36, False), + "decal_puddle": ("decal_puddle.png", 40, False), } # full-art images: downscale + copy, never chroma-keyed (they aren't sprites) @@ -73,7 +113,11 @@ def chroma_key(im): return Image.fromarray(arr, "RGBA") +missing = [] for key, (fname, size, is_tile) in TABLE.items(): + if not (SRC / fname).exists(): + missing.append(fname) # not generated yet — skip, don't crash the batch + continue im = Image.open(SRC / fname).convert("RGBA") if is_tile: im = im.resize((size, size), Image.LANCZOS) @@ -92,9 +136,14 @@ for key, (fname, size, is_tile) in TABLE.items(): print(f"{key:10s} <- {fname:24s} {size}px {'tile' if is_tile else 'keyed'}") for key, (fname, width) in ART.items(): + if not (SRC / fname).exists(): + missing.append(fname) + continue im = Image.open(SRC / fname).convert("RGB") im.thumbnail((width, width * 2), Image.LANCZOS) im.save(OUT / f"{key}.png") print(f"{key:10s} <- {fname:24s} art {im.width}x{im.height}") -print(f"\nwrote {len(TABLE) + len(ART)} textures to {OUT}") +if missing: + print(f"\nskipped {len(missing)} not-yet-generated: {', '.join(sorted(missing))}") +print(f"\nwrote {len(TABLE) + len(ART) - len(missing)} textures to {OUT}") diff --git a/assets/queue4.json b/assets/queue4.json new file mode 100644 index 0000000..a0e5539 --- /dev/null +++ b/assets/queue4.json @@ -0,0 +1,42 @@ +{ + "_comment": "Batch 4: wall VARIANTS (kills tile repetition — 3 per theme) + floor decals. Seeds 120+.", + "style": { + "sprite": "16-bit SNES-era pixel art game sprite, top-down view, bold clean black outline, vibrant saturated colors, single centered subject, flat solid magenta #ff00ff background, no text, no watermark, no drop shadow", + "wall": "16-bit pixel art, top-down game wall tile, seamless, dark, no text" + }, + "defaults": { "model": "flux2-klein-4b", "steps": 4 }, + "items": [ + { "name": "wall_crate_b", "seed": 120, "style": "wall", "w": 512, "h": 512, "prompt": "stacked wooden vinyl record crates from above, one crate tipped and spilling records, brown" }, + { "name": "wall_crate_c", "seed": 121, "style": "wall", "w": 512, "h": 512, "prompt": "wooden record crates stacked with a hand-written SALE sign taped on, brown" }, + { "name": "wall_crate_d", "seed": 122, "style": "wall", "w": 512, "h": 512, "prompt": "battered old wooden record crates, splintered corners, faded paint, brown" }, + + { "name": "wall_shelf_b", "seed": 123, "style": "wall", "w": 512, "h": 512, "prompt": "record shop shelving with a gap where records are missing, colourful spines" }, + { "name": "wall_shelf_c", "seed": 124, "style": "wall", "w": 512, "h": 512, "prompt": "record shelving crammed with LPs and a few sleeves face-out showing cover art" }, + { "name": "wall_shelf_d", "seed": 125, "style": "wall", "w": 512, "h": 512, "prompt": "tall record shelf with alphabet divider cards poking up between the records" }, + + { "name": "wall_poster_b", "seed": 126, "style": "wall", "w": 512, "h": 512, "prompt": "brick wall covered in torn half-peeled rave flyers, layers of old paper" }, + { "name": "wall_poster_c", "seed": 127, "style": "wall", "w": 512, "h": 512, "prompt": "club wall of neon spray-painted graffiti tags over concrete" }, + { "name": "wall_poster_d", "seed": 128, "style": "wall", "w": 512, "h": 512, "prompt": "black club wall with acoustic foam panels and a single red exit light" }, + + { "name": "wall_warehouse_b", "seed": 129, "style": "wall", "w": 512, "h": 512, "prompt": "wall of stacked bass bins and subwoofers, black cones, rave sound system" }, + { "name": "wall_warehouse_c", "seed": 130, "style": "wall", "w": 512, "h": 512, "prompt": "warehouse wall of flight cases and road cases stacked with cables draped over" }, + { "name": "wall_warehouse_d", "seed": 131, "style": "wall", "w": 512, "h": 512, "prompt": "corrugated metal warehouse wall with rust streaks and a padlocked roller door" }, + + { "name": "wall_festival_b", "seed": 132, "style": "wall", "w": 512, "h": 512, "prompt": "festival barrier fence with a torn sponsor banner flapping on it" }, + { "name": "wall_festival_c", "seed": 133, "style": "wall", "w": 512, "h": 512, "prompt": "stack of hay bales and wooden pallets forming a festival barricade" }, + { "name": "wall_festival_d", "seed": 134, "style": "wall", "w": 512, "h": 512, "prompt": "row of portable toilets and generator units seen from above, festival infrastructure" }, + + { "name": "wall_archive_b", "seed": 135, "style": "wall", "w": 512, "h": 512, "prompt": "dark archive shelving of collapsing water-damaged record boxes, mould stains" }, + { "name": "wall_archive_c", "seed": 136, "style": "wall", "w": 512, "h": 512, "prompt": "dark basement wall of rusted filing cabinets stuffed with paper sleeves" }, + { "name": "wall_archive_d", "seed": 137, "style": "wall", "w": 512, "h": 512, "prompt": "dark stone basement wall draped in thick cobwebs and dangling cables" }, + + { "name": "decal_cables", "seed": 140, "style": "sprite", "w": 384, "h": 384, "prompt": "a coiled tangle of black audio cables lying on the floor, seen from above" }, + { "name": "decal_cups", "seed": 141, "style": "sprite", "w": 384, "h": 384, "prompt": "crushed empty plastic pint cups lying on the floor, seen from above" }, + { "name": "decal_flyers", "seed": 142, "style": "sprite", "w": 384, "h": 384, "prompt": "scattered rave flyers dropped on the floor, seen from above" }, + { "name": "decal_confetti", "seed": 143, "style": "sprite", "w": 384, "h": 384, "prompt": "a scatter of colourful confetti and streamers on the ground, seen from above" }, + { "name": "decal_tape", "seed": 144, "style": "sprite", "w": 384, "h": 384, "prompt": "strips of silver gaffer tape stuck on the floor in an X, seen from above" }, + { "name": "decal_sleeve", "seed": 145, "style": "sprite", "w": 384, "h": 384, "prompt": "a discarded empty record sleeve lying face down on the floor, seen from above" }, + { "name": "decal_dust", "seed": 146, "style": "sprite", "w": 384, "h": 384, "prompt": "a pile of grey dust and cobweb fluff on a dark floor, seen from above" }, + { "name": "decal_puddle", "seed": 147, "style": "sprite", "w": 384, "h": 384, "prompt": "a dark spilled drink puddle on the floor, seen from above" } + ] +} diff --git a/assets/queue5.json b/assets/queue5.json new file mode 100644 index 0000000..917f58b --- /dev/null +++ b/assets/queue5.json @@ -0,0 +1,9 @@ +{ + "_comment": "Coarser club floor — the fine checker read as noise at 96px.", + "style": { "tile": "16-bit pixel art, seamless tileable top-down floor texture, LARGE bold simple shapes, flat even lighting, no objects, no perspective, no text" }, + "defaults": { "model": "flux2-klein-4b", "steps": 4 }, + "items": [ + { "name": "floor_club_b", "seed": 160, "style": "tile", "w": 512, "h": 512, "prompt": "nightclub dancefloor with very large bold black and white checker squares, only 4 big squares total, worn and scuffed" }, + { "name": "floor_club_c", "seed": 161, "style": "tile", "w": 512, "h": 512, "prompt": "dark scuffed nightclub dancefloor, plain dark tiles with subtle scratches and scuff marks, minimal pattern" } + ] +} diff --git a/public/promo/banner.jpg b/public/promo/banner.jpg new file mode 100644 index 0000000..2e80880 Binary files /dev/null and b/public/promo/banner.jpg differ diff --git a/public/promo/cabinet.jpg b/public/promo/cabinet.jpg new file mode 100644 index 0000000..304d72a Binary files /dev/null and b/public/promo/cabinet.jpg differ diff --git a/public/sprites/decal_cables.png b/public/sprites/decal_cables.png new file mode 100644 index 0000000..196fa67 Binary files /dev/null and b/public/sprites/decal_cables.png differ diff --git a/public/sprites/decal_confetti.png b/public/sprites/decal_confetti.png new file mode 100644 index 0000000..71f4a30 Binary files /dev/null and b/public/sprites/decal_confetti.png differ diff --git a/public/sprites/decal_cups.png b/public/sprites/decal_cups.png new file mode 100644 index 0000000..58b7809 Binary files /dev/null and b/public/sprites/decal_cups.png differ diff --git a/public/sprites/decal_dust.png b/public/sprites/decal_dust.png new file mode 100644 index 0000000..a4ac0c4 Binary files /dev/null and b/public/sprites/decal_dust.png differ diff --git a/public/sprites/decal_flyers.png b/public/sprites/decal_flyers.png new file mode 100644 index 0000000..2781c13 Binary files /dev/null and b/public/sprites/decal_flyers.png differ diff --git a/public/sprites/decal_puddle.png b/public/sprites/decal_puddle.png new file mode 100644 index 0000000..3cfda13 Binary files /dev/null and b/public/sprites/decal_puddle.png differ diff --git a/public/sprites/decal_sleeve.png b/public/sprites/decal_sleeve.png new file mode 100644 index 0000000..3bbc717 Binary files /dev/null and b/public/sprites/decal_sleeve.png differ diff --git a/public/sprites/decal_tape.png b/public/sprites/decal_tape.png new file mode 100644 index 0000000..85c3330 Binary files /dev/null and b/public/sprites/decal_tape.png differ diff --git a/public/sprites/firemarshal.png b/public/sprites/firemarshal.png new file mode 100644 index 0000000..6568dec Binary files /dev/null and b/public/sprites/firemarshal.png differ diff --git a/public/sprites/floor_checker.png b/public/sprites/floor_checker.png index fb28ef6..3cc3d71 100644 Binary files a/public/sprites/floor_checker.png and b/public/sprites/floor_checker.png differ diff --git a/public/sprites/player_promoter.png b/public/sprites/player_promoter.png new file mode 100644 index 0000000..970e5e4 Binary files /dev/null and b/public/sprites/player_promoter.png differ diff --git a/public/sprites/player_selector.png b/public/sprites/player_selector.png new file mode 100644 index 0000000..86c3239 Binary files /dev/null and b/public/sprites/player_selector.png differ diff --git a/public/sprites/player_soundtech.png b/public/sprites/player_soundtech.png new file mode 100644 index 0000000..5fd166b Binary files /dev/null and b/public/sprites/player_soundtech.png differ diff --git a/public/sprites/scalper.png b/public/sprites/scalper.png new file mode 100644 index 0000000..c85890c Binary files /dev/null and b/public/sprites/scalper.png differ diff --git a/public/sprites/speaker.png b/public/sprites/speaker.png new file mode 100644 index 0000000..fbee091 Binary files /dev/null and b/public/sprites/speaker.png differ diff --git a/public/sprites/strobe.png b/public/sprites/strobe.png new file mode 100644 index 0000000..eda4738 Binary files /dev/null and b/public/sprites/strobe.png differ diff --git a/public/sprites/turntable.png b/public/sprites/turntable.png new file mode 100644 index 0000000..1b3c863 Binary files /dev/null and b/public/sprites/turntable.png differ diff --git a/public/sprites/wall_archive_b.png b/public/sprites/wall_archive_b.png new file mode 100644 index 0000000..3736d2a Binary files /dev/null and b/public/sprites/wall_archive_b.png differ diff --git a/public/sprites/wall_archive_c.png b/public/sprites/wall_archive_c.png new file mode 100644 index 0000000..c835488 Binary files /dev/null and b/public/sprites/wall_archive_c.png differ diff --git a/public/sprites/wall_archive_d.png b/public/sprites/wall_archive_d.png new file mode 100644 index 0000000..1d823d8 Binary files /dev/null and b/public/sprites/wall_archive_d.png differ diff --git a/public/sprites/wall_b.png b/public/sprites/wall_b.png new file mode 100644 index 0000000..885d2ab Binary files /dev/null and b/public/sprites/wall_b.png differ diff --git a/public/sprites/wall_c.png b/public/sprites/wall_c.png new file mode 100644 index 0000000..eb8fae9 Binary files /dev/null and b/public/sprites/wall_c.png differ diff --git a/public/sprites/wall_d.png b/public/sprites/wall_d.png new file mode 100644 index 0000000..845b4f1 Binary files /dev/null and b/public/sprites/wall_d.png differ diff --git a/public/sprites/wall_festival_b.png b/public/sprites/wall_festival_b.png new file mode 100644 index 0000000..dbcfd58 Binary files /dev/null and b/public/sprites/wall_festival_b.png differ diff --git a/public/sprites/wall_festival_c.png b/public/sprites/wall_festival_c.png new file mode 100644 index 0000000..94adc37 Binary files /dev/null and b/public/sprites/wall_festival_c.png differ diff --git a/public/sprites/wall_festival_d.png b/public/sprites/wall_festival_d.png new file mode 100644 index 0000000..9d868ab Binary files /dev/null and b/public/sprites/wall_festival_d.png differ diff --git a/public/sprites/wall_poster_b.png b/public/sprites/wall_poster_b.png new file mode 100644 index 0000000..6f29020 Binary files /dev/null and b/public/sprites/wall_poster_b.png differ diff --git a/public/sprites/wall_poster_c.png b/public/sprites/wall_poster_c.png new file mode 100644 index 0000000..cc0dbae Binary files /dev/null and b/public/sprites/wall_poster_c.png differ diff --git a/public/sprites/wall_poster_d.png b/public/sprites/wall_poster_d.png new file mode 100644 index 0000000..c26ac94 Binary files /dev/null and b/public/sprites/wall_poster_d.png differ diff --git a/public/sprites/wall_shelf_b.png b/public/sprites/wall_shelf_b.png new file mode 100644 index 0000000..c01a2d7 Binary files /dev/null and b/public/sprites/wall_shelf_b.png differ diff --git a/public/sprites/wall_shelf_c.png b/public/sprites/wall_shelf_c.png new file mode 100644 index 0000000..fcf81af Binary files /dev/null and b/public/sprites/wall_shelf_c.png differ diff --git a/public/sprites/wall_shelf_d.png b/public/sprites/wall_shelf_d.png new file mode 100644 index 0000000..695396a Binary files /dev/null and b/public/sprites/wall_shelf_d.png differ diff --git a/public/sprites/wall_warehouse_b.png b/public/sprites/wall_warehouse_b.png new file mode 100644 index 0000000..3968bfb Binary files /dev/null and b/public/sprites/wall_warehouse_b.png differ diff --git a/public/sprites/wall_warehouse_c.png b/public/sprites/wall_warehouse_c.png new file mode 100644 index 0000000..48bebba Binary files /dev/null and b/public/sprites/wall_warehouse_c.png differ diff --git a/public/sprites/wall_warehouse_d.png b/public/sprites/wall_warehouse_d.png new file mode 100644 index 0000000..890f7cd Binary files /dev/null and b/public/sprites/wall_warehouse_d.png differ diff --git a/src/GameScene.js b/src/GameScene.js index ad70094..c5ae21f 100644 --- a/src/GameScene.js +++ b/src/GameScene.js @@ -28,10 +28,19 @@ const QUEEN_BROOD_MS = 5000; // queen spits mites this often // texture keys loaded from public/sprites/.png (see assets/process_sprites.py) const TEXTURE_KEYS = [ 'floor', 'floor_carpet', 'floor_checker', 'floor_archive', 'floor_warehouse', 'floor_festival', - 'wall', 'wall_shelf', 'wall_poster', 'wall_archive', 'wall_warehouse', 'wall_festival', - 'player', 'poser', 'nerd', 'pirate', 'ghoul', 'mite', 'queen', 'soundguy', 'generator', - 'gatekeeper', 'shazam', 'warped', 'conductor', 'rpm78', + // 4 wall variants per theme — picked per-tile so mazes don't read as wallpaper + 'wall', 'wall_b', 'wall_c', 'wall_d', + 'wall_shelf', 'wall_shelf_b', 'wall_shelf_c', 'wall_shelf_d', + 'wall_poster', 'wall_poster_b', 'wall_poster_c', 'wall_poster_d', + 'wall_warehouse', 'wall_warehouse_b', 'wall_warehouse_c', 'wall_warehouse_d', + 'wall_festival', 'wall_festival_b', 'wall_festival_c', 'wall_festival_d', + 'wall_archive', 'wall_archive_b', 'wall_archive_c', 'wall_archive_d', + 'player', 'player_selector', 'player_promoter', 'player_soundtech', + 'poser', 'nerd', 'pirate', 'ghoul', 'mite', 'queen', 'soundguy', 'generator', + 'gatekeeper', 'shazam', 'warped', 'scalper', 'firemarshal', 'conductor', 'rpm78', 'exit', 'door', 'espresso', 'lanyard', 'airhorn', 'vinyl', 'gold', 'lamp', + 'decal_cables', 'decal_cups', 'decal_flyers', 'decal_confetti', + 'decal_tape', 'decal_sleeve', 'decal_dust', 'decal_puddle', 'port_digger', 'port_selector', 'port_promoter', 'port_soundtech', 'logo', ]; const BOSS_SCORE = { conductor: 5000, rpm78: 10000 }; @@ -45,10 +54,10 @@ const RPM_SHARDS_MS = 6000; // Gauntlet II classes: stat tweaks only, no new mechanics. const CLASSES = { - digger: { name: 'THE DIGGER', desc: '+25% speed', speed: 1.25, dmg: 1, lanyards: 0, airhorns: 1, sgDrain: 1, tint: 0xffffff }, - selector: { name: 'THE SELECTOR', desc: 'double damage', speed: 1, dmg: 2, lanyards: 0, airhorns: 1, sgDrain: 1, tint: 0xffe080 }, - promoter: { name: 'THE PROMOTER', desc: 'starts loaded', speed: 1, dmg: 1, lanyards: 1, airhorns: 2, sgDrain: 1, tint: 0xffa0ff }, - soundtech: { name: 'THE SOUND TECH', desc: 'sound guy resistant', speed: 1, dmg: 1, lanyards: 0, airhorns: 1, sgDrain: 0.4, tint: 0xa0ffa0 }, + digger: { name: 'THE DIGGER', desc: '+25% speed', speed: 1.25, dmg: 1, lanyards: 0, airhorns: 1, sgDrain: 1, tex: 'player' }, + selector: { name: 'THE SELECTOR', desc: 'double damage', speed: 1, dmg: 2, lanyards: 0, airhorns: 1, sgDrain: 1, tex: 'player_selector' }, + promoter: { name: 'THE PROMOTER', desc: 'starts loaded', speed: 1, dmg: 1, lanyards: 1, airhorns: 2, sgDrain: 1, tex: 'player_promoter' }, + soundtech: { name: 'THE SOUND TECH', desc: 'sound guy resistant', speed: 1, dmg: 1, lanyards: 0, airhorns: 1, sgDrain: 0.4, tex: 'player_soundtech' }, }; const ENEMY_TYPES = { @@ -59,6 +68,7 @@ const ENEMY_TYPES = { mite: { tex: 'mite', speed: 240, touch: 15, hp: 1, size: 18 }, // tiny, fast, spawns in packs queen: { tex: 'queen', speed: 40, touch: 60, hp: 12, size: 70, boss: true }, // broods mites gatekeeper: { tex: 'gatekeeper', speed: 60, touch: 50, hp: 4, size: 40 }, // heavy blocker + scalper: { tex: 'scalper', speed: 230, touch: 25, hp: 2, size: 34 }, // goes for the gold, not you shazam: { tex: 'shazam', speed: 0, touch: 20, hp: 1, size: 34, ranged: true }, // turret: "what song is this?" warped: { tex: 'warped', speed: 150, touch: 40, hp: 1, size: 30, wobble: true }, // wobbles like a bad press conductor: { tex: 'conductor', speed: 55, touch: 60, hp: 20, size: 60, boss: true }, // buffs the room @@ -66,13 +76,28 @@ const ENEMY_TYPES = { }; // Level themes cycle with level index; mix = weighted generator spawn table. +// walls: 4 variants each, picked per-tile so a 300-tile maze doesn't read as wallpaper. +// decals: scattered floor litter, purely decorative. const THEMES = [ - { name: 'RECORD SHOP', pool: 'shop', floor: 'floor', wall: 'wall', mix: [['poser', 0.7], ['nerd', 0.3]] }, - { name: 'RECORD FAIR', pool: 'fair', floor: 'floor_carpet', wall: 'wall_shelf', mix: [['poser', 0.5], ['nerd', 0.25], ['pirate', 0.25]] }, - { name: 'CLUB NIGHT', pool: 'club', floor: 'floor_checker', wall: 'wall_poster', mix: [['poser', 0.4], ['nerd', 0.2], ['pirate', 0.2], ['ghoul', 0.2]] }, - { name: 'WAREHOUSE RAVE', pool: 'warehouse', floor: 'floor_warehouse', wall: 'wall_warehouse', mix: [['poser', 0.3], ['shazam', 0.25], ['gatekeeper', 0.25], ['nerd', 0.2]] }, - { name: 'FESTIVAL GROUNDS', pool: 'festival', floor: 'floor_festival', wall: 'wall_festival', mix: [['poser', 0.3], ['warped', 0.3], ['pirate', 0.2], ['ghoul', 0.2]] }, - { name: 'THE ARCHIVE', pool: 'archive', floor: 'floor_archive', wall: 'wall_archive', dark: true, mix: [['mite', 0.45], ['ghoul', 0.3], ['poser', 0.25]] }, + { name: 'RECORD SHOP', pool: 'shop', floor: 'floor', mix: [['poser', 0.7], ['nerd', 0.3]], + walls: ['wall', 'wall_b', 'wall_c', 'wall_d'], + decals: ['decal_sleeve', 'decal_flyers', 'decal_dust'] }, + { name: 'RECORD FAIR', pool: 'fair', floor: 'floor_carpet', mix: [['poser', 0.5], ['nerd', 0.25], ['pirate', 0.25]], + walls: ['wall_shelf', 'wall_shelf_b', 'wall_shelf_c', 'wall_shelf_d'], + decals: ['decal_flyers', 'decal_sleeve', 'decal_cups'] }, + // floorTint: busy floors get knocked back hard so sprites stay readable on top + { name: 'CLUB NIGHT', pool: 'club', floor: 'floor_checker', floorTint: 0x3a3a3a, mix: [['poser', 0.35], ['nerd', 0.2], ['pirate', 0.2], ['scalper', 0.1], ['ghoul', 0.15]], + walls: ['wall_poster', 'wall_poster_b', 'wall_poster_c', 'wall_poster_d'], + decals: ['decal_cups', 'decal_puddle', 'decal_flyers', 'decal_tape'] }, + { name: 'WAREHOUSE RAVE', pool: 'warehouse', floor: 'floor_warehouse', mix: [['poser', 0.3], ['shazam', 0.25], ['gatekeeper', 0.25], ['nerd', 0.2]], + walls: ['wall_warehouse', 'wall_warehouse_b', 'wall_warehouse_c', 'wall_warehouse_d'], + decals: ['decal_cables', 'decal_tape', 'decal_cups', 'decal_puddle'] }, + { name: 'FESTIVAL GROUNDS', pool: 'festival', floor: 'floor_festival', mix: [['poser', 0.25], ['warped', 0.25], ['scalper', 0.2], ['pirate', 0.15], ['ghoul', 0.15]], + walls: ['wall_festival', 'wall_festival_b', 'wall_festival_c', 'wall_festival_d'], + decals: ['decal_confetti', 'decal_cups', 'decal_flyers'] }, + { name: 'THE ARCHIVE', pool: 'archive', floor: 'floor_archive', dark: true, mix: [['mite', 0.45], ['ghoul', 0.3], ['poser', 0.25]], + walls: ['wall_archive', 'wall_archive_b', 'wall_archive_c', 'wall_archive_d'], + decals: ['decal_dust', 'decal_sleeve', 'decal_cables'] }, ]; // boss arenas drop into the cycle at fixed depths (each 12-level loop has both) const bossFor = (index) => (index % 12 === 7 ? 'conductor' : index % 12 === 11 ? 'rpm78' : null); @@ -351,6 +376,8 @@ export default class GameScene extends Phaser.Scene { this.lamps = []; if (this.lampSprites) this.lampSprites.forEach((l) => l.destroy()); this.lampSprites = []; + if (this.decals) this.decals.forEach((d) => d.destroy()); + this.decals = []; this.walls.clear(true, true); this.doors.clear(true, true); this.pickups.clear(true, true); @@ -375,7 +402,7 @@ export default class GameScene extends Phaser.Scene { .tileSprite(0, 0, worldW, worldH, this.theme.floor) .setOrigin(0) .setDepth(-10) - .setTint(0x555555); // darken so walls read as raised + .setTint(this.theme.floorTint || 0x555555); // darken so walls read as raised for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { @@ -384,7 +411,7 @@ export default class GameScene extends Phaser.Scene { const y = r * TILE + TILE / 2; switch (ch) { case 'W': - this.walls.create(x, y, this.theme.wall); + this.walls.create(x, y, this.wallTexAt(c, r)); break; case 'D': this.doors.create(x, y, 'door'); @@ -415,6 +442,12 @@ export default class GameScene extends Phaser.Scene { this.say(LINES.soundGuy); break; } + case 'F': { + // the Fire Marshal: same unkillable wall-phasing stalker, deeper levels + this.soundGuys.create(x, y, this.textures.exists('firemarshal') ? 'firemarshal' : 'soundguy'); + this.say(LINES.fireMarshal); + break; + } case 'Q': { this.spawnEnemy({ x, y }, 'queen'); this.say(LINES.queen); @@ -451,6 +484,8 @@ export default class GameScene extends Phaser.Scene { } } + this.scatterDecals(map, cols, rows); + this.density = this.densityBase + index * 0.08; // deeper = busier this.levelStartTime = this.time.now; this.trainspotProgress = 0; @@ -462,6 +497,37 @@ export default class GameScene extends Phaser.Scene { } } + // Stable per-tile wall variant: same tile always gets the same texture, but + // neighbours differ — so a 300-tile maze stops reading as wallpaper. + wallTexAt(col, row) { + const variants = this.theme.walls.filter((k) => this.textures.exists(k)); + if (!variants.length) return 'wall'; + const h = Math.abs(Math.imul(col * 73856093 ^ row * 19349663, 0x45d9f3b)); + return variants[h % variants.length]; + } + + // Scatter floor litter on open tiles. Decoration only — no bodies, no gameplay. + scatterDecals(map, cols, rows) { + const keys = (this.theme.decals || []).filter((k) => this.textures.exists(k)); + if (!keys.length) return; + const rnd = new Phaser.Math.RandomDataGenerator([`decal${this.levelIndex}`]); + const budget = Math.floor(cols * rows * 0.05); + for (let i = 0; i < budget; i++) { + const r = rnd.between(1, rows - 2); + const c = rnd.between(1, cols - 2); + if (map[r][c] !== '.') continue; + this.decals.push( + this.add + .image(c * TILE + rnd.between(-10, 10) + TILE / 2, r * TILE + rnd.between(-10, 10) + TILE / 2, + rnd.pick(keys)) + .setDepth(-8) + .setAlpha(0.45) + .setTint(0x888888) // litter sits into the floor, never competes with sprites + .setAngle(rnd.between(0, 359)), + ); + } + } + // Deterministic maze for levels past the hand-authored ones. Same level = same maze. genLevel(index) { const rnd = new Phaser.Math.RandomDataGenerator([String(index * 7919)]); @@ -514,6 +580,7 @@ export default class GameScene extends Phaser.Scene { for (let i = 0; i < 3; i++) drop('R'); drop('A'); drop('S'); + if (index >= 6) drop('F'); // the fire marshal starts showing up once it's late const theme = THEMES[index % THEMES.length]; if (theme.dark) { drop('Q'); @@ -698,10 +765,11 @@ export default class GameScene extends Phaser.Scene { }); } - nearestPickup(from) { + nearestPickup(from, type) { let best = null; let bestD = Infinity; this.pickups.getChildren().forEach((p) => { + if (type && p.getData('type') !== type) return; const d = Phaser.Math.Distance.Between(from.x, from.y, p.x, p.y); if (d < bestD) { bestD = d; @@ -766,7 +834,7 @@ export default class GameScene extends Phaser.Scene { if (type === 'conductor') { // teleports away every 5 hits — handled in hitEnemy; drifts slowly here } - if (type === 'pirate') { + if (type === 'pirate' || type === 'scalper') { if (e.getData('carry')) { const home = e.getData('home'); if (Phaser.Math.Distance.Between(e.x, e.y, home.x, home.y) < 24) { @@ -775,7 +843,8 @@ export default class GameScene extends Phaser.Scene { } target = home; } else { - target = this.nearestPickup(e) || this.player; + // the scalper only wants the gold; the pirate grabs anything + target = this.nearestPickup(e, type === 'scalper' ? 'gold' : null) || this.player; } } this.physics.moveToObject(e, target, e.getData('speed') * speedBuff); @@ -992,7 +1061,9 @@ export default class GameScene extends Phaser.Scene { } pirateGrab(e, item) { - if (!e.active || e.getData('type') !== 'pirate' || e.getData('carry')) return; + const t = e.getData('type'); + if (!e.active || (t !== 'pirate' && t !== 'scalper') || e.getData('carry')) return; + if (t === 'scalper' && item.getData('type') !== 'gold') return; e.setData('carry', { type: item.getData('type'), tex: item.texture.key }); item.destroy(); } @@ -1045,7 +1116,7 @@ export default class GameScene extends Phaser.Scene { if (this.state !== 'PLAYING') return; this.vibe -= amount; this.player.setTintFill(0xff0000); - this.time.delayedCall(100, () => this.player.setTint(this.cls.tint)); + this.time.delayedCall(100, () => this.player.clearTint()); if (this.trainspotProgress > 0) this.trainspotProgress = 0; // trainspot breaks on any hit this.checkThresholds(); if (this.vibe <= 0) this.gameOver(); @@ -1202,7 +1273,8 @@ export default class GameScene extends Phaser.Scene { startGame(clsKey) { this.cls = CLASSES[clsKey] || CLASSES.digger; - this.player.setTint(this.cls.tint); + this.player.clearTint(); + if (this.textures.exists(this.cls.tex)) this.player.setTexture(this.cls.tex); this.physics.resume(); this.resetStats(); this.hideOverlay(); diff --git a/src/announcer.js b/src/announcer.js index 474706e..de6b31b 100644 --- a/src/announcer.js +++ b/src/announcer.js @@ -56,4 +56,6 @@ export const LINES = { conductorDead: ['conductorDead', 'The baton has fallen. The floor is yours.'], rpm78: ['rpm78', 'Seventy eight revolutions. No name. No mercy.'], rpm78Dead: ['rpm78Dead', 'It has a name now. Yours.'], + fireMarshal: ['fireMarshal', 'The fire marshal is checking capacity.'], + scalper: ['scalper', 'Scalper on the floor. Mind your grails.'], };