diff --git a/assets/collect.py b/assets/collect.py new file mode 100644 index 0000000..b8dfd77 --- /dev/null +++ b/assets/collect.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Copy finished FLUX assets out of MODELBEAST into assets/generated/.png. + +Reads queue.lock.json (job_id/name/seed written by enqueue.py), finds each +generated image in the MB asset store by its seed-based filename, and copies it +in under the friendly asset name. Run after the MB jobs finish. + + /opt/homebrew/bin/uv run python assets/collect.py +""" +import json, sys, shutil +from pathlib import Path + +MB = Path.home() / "MODELBEAST" +sys.path.insert(0, str(MB)) +import server.db as db # noqa: E402 + +HERE = Path(__file__).resolve().parent +lock = json.loads((HERE / "queue.lock.json").read_text()) +outdir = HERE / "generated" +outdir.mkdir(exist_ok=True) + +con = db.connect() +done = missing = 0 +for it in lock: + fname = f"flux_flux2-klein-4b_s{it['seed']}.png" + row = con.execute("SELECT path FROM assets WHERE name=? ORDER BY created_at DESC LIMIT 1", (fname,)).fetchone() + if row and Path(row["path"]).exists(): + shutil.copy(row["path"], outdir / f"{it['name']}.png") + done += 1 + else: + print(f" not ready: {it['name']}") + missing += 1 +print(f"collected {done}/{len(lock)} into {outdir}" + (f" ({missing} not ready)" if missing else "")) diff --git a/assets/process_sprites.py b/assets/process_sprites.py new file mode 100644 index 0000000..a2fdcf1 --- /dev/null +++ b/assets/process_sprites.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Turn the raw FLUX assets into game-ready textures in public/sprites/. + +Sprites (magenta #ff00ff background) get chroma-keyed to transparency, autocropped, +and centered on a square canvas. Tiles (full-bleed textures) are just resized square. +Output filename = the texture key GameScene.js loads. + + /opt/homebrew/bin/uv run --with pillow --with numpy python assets/process_sprites.py +""" +from pathlib import Path +from PIL import Image +import numpy as np + +HERE = Path(__file__).resolve().parent +SRC = HERE / "generated" +OUT = HERE.parent / "public" / "sprites" +OUT.mkdir(parents=True, exist_ok=True) + +# game texture key -> (source file, size px, is_tile) +TABLE = { + "floor": ("floor_parquet.png", 64, True), + "wall": ("wall_crate.png", 32, True), + "player": ("player_digger.png", 32, False), + "poser": ("enemy_poser.png", 32, False), + "nerd": ("enemy_gearnerd.png", 32, False), + "soundguy": ("enemy_soundguy.png", 32, False), + "generator": ("gen_discount_bin.png", 32, False), + "exit": ("prop_djbooth.png", 32, False), + "door": ("prop_velvet_rope.png", 32, False), + "espresso": ("pickup_espresso.png", 26, False), + "lanyard": ("pickup_lanyard.png", 26, False), + "airhorn": ("pickup_airhorn.png", 26, False), + "vinyl": ("pickup_vinyl.png", 18, False), +} + + +def chroma_key(im): + arr = np.array(im.convert("RGBA")) + r, g, b = arr[..., 0].astype(int), arr[..., 1].astype(int), arr[..., 2].astype(int) + # magenta: high red+blue, low green, red~=blue (catches the darker shadow halo too) + mask = (r > 135) & (b > 135) & (g < 100) & (np.abs(r - b) < 70) + arr[..., 3][mask] = 0 + return Image.fromarray(arr, "RGBA") + + +for key, (fname, size, is_tile) in TABLE.items(): + im = Image.open(SRC / fname).convert("RGBA") + if is_tile: + im = im.resize((size, size), Image.LANCZOS) + else: + im = chroma_key(im) + bbox = im.getbbox() + if bbox: + im = im.crop(bbox) + im.thumbnail((size, size), Image.LANCZOS) + canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + canvas.paste(im, ((size - im.width) // 2, (size - im.height) // 2), im) + im = canvas + im.save(OUT / f"{key}.png") + print(f"{key:10s} <- {fname:24s} {size}px {'tile' if is_tile else 'keyed'}") + +print(f"\nwrote {len(TABLE)} textures to {OUT}") diff --git a/public/sprites/airhorn.png b/public/sprites/airhorn.png new file mode 100644 index 0000000..57dd4e6 Binary files /dev/null and b/public/sprites/airhorn.png differ diff --git a/public/sprites/door.png b/public/sprites/door.png new file mode 100644 index 0000000..5484e5f Binary files /dev/null and b/public/sprites/door.png differ diff --git a/public/sprites/espresso.png b/public/sprites/espresso.png new file mode 100644 index 0000000..5d1d7ab Binary files /dev/null and b/public/sprites/espresso.png differ diff --git a/public/sprites/exit.png b/public/sprites/exit.png new file mode 100644 index 0000000..760f08e Binary files /dev/null and b/public/sprites/exit.png differ diff --git a/public/sprites/floor.png b/public/sprites/floor.png new file mode 100644 index 0000000..7c87aba Binary files /dev/null and b/public/sprites/floor.png differ diff --git a/public/sprites/generator.png b/public/sprites/generator.png new file mode 100644 index 0000000..1021106 Binary files /dev/null and b/public/sprites/generator.png differ diff --git a/public/sprites/lanyard.png b/public/sprites/lanyard.png new file mode 100644 index 0000000..a04818a Binary files /dev/null and b/public/sprites/lanyard.png differ diff --git a/public/sprites/nerd.png b/public/sprites/nerd.png new file mode 100644 index 0000000..0ac9dfe Binary files /dev/null and b/public/sprites/nerd.png differ diff --git a/public/sprites/player.png b/public/sprites/player.png new file mode 100644 index 0000000..d0605c7 Binary files /dev/null and b/public/sprites/player.png differ diff --git a/public/sprites/poser.png b/public/sprites/poser.png new file mode 100644 index 0000000..ebe3d6a Binary files /dev/null and b/public/sprites/poser.png differ diff --git a/public/sprites/soundguy.png b/public/sprites/soundguy.png new file mode 100644 index 0000000..6202074 Binary files /dev/null and b/public/sprites/soundguy.png differ diff --git a/public/sprites/vinyl.png b/public/sprites/vinyl.png new file mode 100644 index 0000000..9e4f03f Binary files /dev/null and b/public/sprites/vinyl.png differ diff --git a/public/sprites/wall.png b/public/sprites/wall.png new file mode 100644 index 0000000..bf13e4f Binary files /dev/null and b/public/sprites/wall.png differ diff --git a/src/GameScene.js b/src/GameScene.js index 6988e3d..cac4ab6 100644 --- a/src/GameScene.js +++ b/src/GameScene.js @@ -21,14 +21,23 @@ const VINYL_LIFE_MS = 1200; const FIRE_COOLDOWN_MS = 180; const TRAINSPOT_MS = 3000; +// texture keys loaded from public/sprites/.png (see assets/process_sprites.py) +const TEXTURE_KEYS = [ + 'floor', 'wall', 'player', 'poser', 'nerd', 'soundguy', 'generator', + 'exit', 'door', 'espresso', 'lanyard', 'airhorn', 'vinyl', +]; + export default class GameScene extends Phaser.Scene { constructor() { super('game'); } + preload() { + for (const key of TEXTURE_KEYS) this.load.image(key, `sprites/${key}.png`); + } + create() { this.cameras.main.setBackgroundColor(0x1a1a1a); - this.makeTextures(); // Persistent groups — created once, refilled each level so colliders survive. this.walls = this.physics.add.staticGroup(); @@ -42,6 +51,7 @@ export default class GameScene extends Phaser.Scene { this.player = this.physics.add.sprite(100, 100, 'player'); this.player.setCollideWorldBounds(true); + this.player.body.setSize(22, 22, true); // body smaller than 32px sprite -> fits corridors // Colliders / overlaps (persistent group refs). this.physics.add.collider(this.player, this.walls); @@ -109,48 +119,6 @@ export default class GameScene extends Phaser.Scene { this.showOverlay('VINYL GAUNTLET\n\npress any key'); } - makeTextures() { - const tex = (key, w, h, draw) => { - const g = this.make.graphics({ x: 0, y: 0, add: false }); - draw(g); - g.generateTexture(key, w, h); - g.destroy(); - }; - tex('wall', TILE, TILE, (g) => { - g.fillStyle(0x6b4423).fillRect(0, 0, TILE, TILE); - g.lineStyle(2, 0x3d2814).strokeRect(1, 1, TILE - 2, TILE - 2); - }); - tex('door', TILE, TILE, (g) => { - g.fillStyle(0xb02020).fillRect(0, 0, TILE, TILE); - g.lineStyle(2, 0x600).strokeRect(1, 1, TILE - 2, TILE - 2); - }); - tex('espresso', 24, 24, (g) => g.fillStyle(0x3ad13a).fillRect(0, 0, 24, 24)); - tex('lanyard', 24, 24, (g) => g.fillStyle(0xffd11a).fillRect(0, 0, 24, 24)); - tex('airhorn', 24, 24, (g) => { - g.fillStyle(0xffffff).fillRect(0, 0, 24, 24); - g.lineStyle(2, 0x888888).strokeRect(1, 1, 22, 22); - }); - tex('generator', 30, 30, (g) => { - g.fillStyle(0x9b30ff).fillRect(0, 0, 30, 30); - g.lineStyle(2, 0x4b0082).strokeRect(1, 1, 28, 28); - }); - tex('exit', TILE, TILE, (g) => { - g.fillStyle(0xff00ff).fillRect(0, 0, TILE, TILE); - g.lineStyle(2, 0xffffff).strokeRect(1, 1, TILE - 2, TILE - 2); - }); - tex('soundguy', 24, 24, (g) => { - g.fillStyle(0x000000).fillRect(0, 0, 24, 24); - g.lineStyle(2, 0xffffff).strokeRect(1, 1, 22, 22); - }); - tex('vinyl', 12, 12, (g) => { - g.fillStyle(0x000000).fillCircle(6, 6, 6); - g.fillStyle(0xffffff).fillCircle(6, 6, 2); - }); - tex('poser', 24, 24, (g) => g.fillStyle(0xff3030).fillTriangle(12, 0, 24, 24, 0, 24)); - tex('nerd', 24, 24, (g) => g.fillStyle(0xff8c1a).fillRect(0, 0, 24, 24)); - tex('player', 26, 26, (g) => g.fillStyle(0x00e5ff).fillCircle(13, 13, 13)); - } - resetStats() { this.vibe = VIBE_MAX; this.lanyards = 0; @@ -185,6 +153,14 @@ export default class GameScene extends Phaser.Scene { this.cameras.main.setBounds(0, 0, worldW, worldH); this.cameras.main.startFollow(this.player, true, 0.15, 0.15); + // Tiled floor behind everything. + if (this.floor) this.floor.destroy(); + this.floor = this.add + .tileSprite(0, 0, worldW, worldH, 'floor') + .setOrigin(0) + .setDepth(-10) + .setTint(0x555555); // darken so the brown crate walls read as raised + for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const ch = map[r][c]; @@ -325,6 +301,7 @@ export default class GameScene extends Phaser.Scene { e.setTexture(poser ? 'poser' : 'nerd'); e.setActive(true).setVisible(true); e.body.enable = true; + e.body.setSize(22, 22, true); // body smaller than 32px sprite e.setPosition(g.x, g.y); e.setData('touch', poser ? POSER_TOUCH_DAMAGE : NERD_TOUCH_DAMAGE); e.setData('speed', poser ? POSER_SPEED : NERD_SPEED);