Files
zenmaid-game/PLAN.md
2026-07-27 10:02:54 -04:00

12 KiB
Raw Blame History

Pot Patrol — Plan

A single-screen, sprite-based, widescreen browser game built with Kaplay (the JS/TS game library, successor to Kaboom.js). Link (AI) rampages across a procedurally generated scene smashing pots; you (the player) chase him and sweep up the shards before he leaves the screen.


1. Concept & core loop

  • Actors: Link (computer-controlled) and Player (you).
  • Link's behavior: wander toward the nearest pot, pick it up, and throw it in a random direction. The pot flies 23 tiles, shatters, and scatters 13 shards onto nearby tiles. Along the way Link gets distracted:
    • Grass — he detours to cut it.
    • Chickens — he hits one once, then it chases him, so he flees briefly before resuming.
  • Player's job: follow Link and clean up shards by touching them.
  • Win: zero shards remain at the moment Link leaves the screen (or after his last pot, once the board is clear).
  • Lose: Link leaves the screen with shards still on the ground.
  • Balance: Link is slightly faster than the player, but loses time to distractions — that lost time is the player's window to catch up.

2. Tech: Kaplay

Kaplay gives us the game loop, rendering, sprites, input, collision, scenes, timers, tweens, vectors, and seeded RNG out of the box — so most of the old "engine" work disappears and we focus on game logic.

  • Load: ES-module import from a CDN, keeping a no-build setup:
    <script type="module">
      import kaplay from "https://unpkg.com/kaplay@3000/dist/kaplay.mjs";
      // ... or import ./src/main.js which imports kaplay
    </script>
    
    Serve over HTTP for module loading: python3 -m http.serverhttp://localhost:8000. (Alternative, better DX: npm create kaplay@latest scaffolds a Vite project. We can switch to that if we want bundling/TS; the plan below is engine-API identical either way.)
  • Init: kaplay({ width: 960, height: 560, background: [...], letterbox: true, global: false }). letterbox keeps the widescreen aspect ratio and scales to the window.
  • Seeded RNG: randSeed(seed) + rand(), randi(), choose() — reproducible scenes for debugging and "retry same level" (no custom PRNG needed).
  • Delta time: dt() for frame-independent movement.
  • Depth: set obj.z = obj.pos.y each frame for top-down y-sorting.

Kaplay features we lean on hardest:

  • state() component — a built-in finite state machine, ideal for Link's AI (onStateEnter/Update/End).
  • addLevel() — turns an ASCII tile map into game objects, matching our procedural grid.
  • area() + onCollide / body({ isStatic }) — collision & obstacle blocking (top-down, gravity 0).
  • scene() / go() — game / win / lose screens.
  • tween() — pot throw arc, sweep juice.

3. Coordinate system

  • Tile-based grid, continuous pixel positions (Kaplay vec2).
  • Config constants (tunable in config.js):
    • TILE = 40 px
    • COLS = 24, ROWS = 14960 × 560 canvas (≈16:9 widescreen).
  • AI reasoning in tile coordinates; movement continuous. addLevel uses tileWidth/tileHeight = TILE.

4. File structure

index.html            # imports src/main.js as a module
styles.css            # page bg, canvas centering
src/
  config.js           # tunable constants (sizes, speeds, counts, radii)
  main.js             # kaplay() init, load assets, register scenes, go("game")
  gen.js              # seeded procedural tile map + entity placement
  sprites.js          # loadSprite / placeholder object factories
  scenes/
    game.js           # main scene: build level, spawn actors, wire collisions
    end.js            # win & lose scenes (result + retry / new-seed)
  actors/
    player.js         # player object + input-driven movement
    link.js           # link object + state() FSM (the AI)
    chicken.js        # chicken idle + chase behavior
  pots.js             # pickup, throw arc, shatter, shard spawning
  hud.js              # shard counter, pots-remaining, dev state readout

Kaplay's built-in RNG/loop/render/input mean no rng.js, render.js, or input.js — that logic is inline via Kaplay APIs.


5. Procedural scene generation (gen.js)

Seeded via randSeed(seed). Produce a level the game scene consumes:

  1. Grid & ground: fill a COLS × ROWS map with floor tiles (optionally 2 variants for noise).
  2. Obstacles: scatter impassable decor (rocks/trees), light count (612), keeping the interior open. Border walkable except one exit gap (§9).
  3. Pots: place POT_COUNT (58) on random passable, unoccupied tiles.
  4. Grass: place GRASS_COUNT (815).
  5. Chickens: place CHICKEN_COUNT (24).
  6. Spawns: Link at one edge; player opposite/center. Keep spawn tiles + their neighbors clear.
  7. Validation: flood-fill from Link's spawn; reroll any unreachable pot (or regenerate) so the level is always solvable.

Two build options (either works — leaning toward B for dynamic control):

  • A. Emit an ASCII map and hand it to addLevel() with a tiles map of component factories per symbol.
  • B. Emit a data structure (tile types + entity list) and add() each object directly — more flexible for entities that move, spawn, and despawn at runtime.

6. Entity model — Kaplay components

Every actor is a Kaplay game object: add([...components, "tag"]).

  • Player: [sprite/placeholder, pos, area(), body(), anchor("center"), "player"], moved from input; PLAYER_SPEED.
  • Link: [..., area(), body(), state("seek", [...]), "link"]; LINK_SPEED (≈1.15× player). AI in the state() FSM (§8).
  • Pot: [..., area(), "pot"]; static until targeted, then handled by pots.js.
  • Shard: [..., area(), "shard"]; removed on player overlap.
  • Grass: [..., area(), "grass"]; destroyed/stubbed when cut.
  • Chicken: [..., area(), body(), state("idle",[...]), "chicken"] (§chickens).
  • Obstacles: [..., area(), body({ isStatic: true }), "obstacle"] — block movers.

Movement helper: obj.move(dir.scale(speed)) toward a target vec2; body resolves collisions against static obstacles. Update z = pos.y for depth.


7. Pots, throwing & shards (pots.js)

  • Target: Link's FSM picks the nearest "pot" still on the board (get("pot") + min distance).
  • Pickup: within INTERACT_DIST, pot attaches to Link (brief wind-up).
  • Throw: pick a random 8-way direction and 23 tiles distance. Animate with tween(): linear travel along the path plus a parabolic height offset (tween a z-height up then down, drawn as vertical sprite offset + a shadow) so it visibly arcs.
  • Shatter on land: spawn 13 shards on the landing tile + random adjacent passable tiles (dedupe to one per tile). addKaboom() / small particle pop + shake() for juice.
  • If the target tile is impassable, land on the nearest passable tile.

States, checked by priority each onStateUpdate:

  1. flee — active for FLEE_TIME after hitting a chicken; run away from the nearest chasing chicken, then return to seek.
  2. distracted — if grass/chicken within DISTRACT_RADIUS, with per-tick probability divert:
    • grass → move adjacent → cut (destroy grass), brief pause → seek.
    • chicken → move adjacent → hit once → triggers chicken chase + Link flee.
  3. seek — move to nearest pot → transition to throw.
  4. throw — run the throw sequence (§7) → back to seek.
  5. leave — no pots remain (or a max-time fuse fires): walk to the exit gap and off-screen → end the round (§9). Detected with offscreen().

"More distractions" = generous DISTRACT_RADIUS + divert probability + flee detours, so Link squanders his speed edge. enterState() transitions log to console in dev; HUD shows current state.


9. Win / lose (scenes/)

  • game scene tracks shardCount (count of "shard" objects) live in the HUD.
  • Link leaves: his leave state + offscreen() ends the round; evaluate:
    • shardCount === 0go("win").
    • shardCount > 0go("lose", { left: shardCount }).
  • Early win: last pot thrown and shardCount hits 0 before he exits → win immediately.
  • End scenes show result + Retry (same seed) and New scene (new seed) (go("game", { seed })), driven by key press.

10. Player input & shard cleanup

  • Kaplay input: onKeyDown("left"/"a"/…) or isKeyDown() to build a direction vec2; normalize so diagonals aren't faster; player.move(dir.scale(PLAYER_SPEED)).
  • Cleanup: player.onCollide("shard", (s) => { destroy(s); shardCount--; }), with an optional sweep tween.
  • Obstacle blocking handled by body() vs static obstacle bodies.

11. Rendering & HUD

Kaplay renders automatically; we control order via z (set each frame to pos.y for actors/chickens; fixed low z for ground/decor, high z for the flying pot + HUD). Sprites via loadSprite; placeholders first so it's playable immediately:

  • Placeholder glyphs with text() — Link 🧝, player 🧹, pot 🏺, shard 🔶, grass 🌿, chicken 🐔, rock 🪨 — or colored rect()/circle() components.
  • HUD (hud.js): shard counter, pots remaining, Link state (dev), win/lose banner — add([text(), fixed(), z(top)]).
  • image-rendering: pixelated on the canvas for crisp sprites.

12. Milestones (build order)

  1. Skeleton: index.html + main.js with kaplay() init, empty game scene, letterboxed canvas. Runs via local server.
  2. Procgen: gen.js builds a seeded level; game scene renders ground, obstacles, pots, grass, chickens, spawns; reachability validated.
  3. Player: input-driven movement + obstacle collision via body().
  4. Link seek+move: state() FSM with seek targeting nearest pot (no throw).
  5. Throw + shards: pickup → tween arc → shatter → 13 shards.
  6. Cleanup + HUD: onCollide shard pickup; live counter.
  7. Distractions: grass cut; chicken hit → chase → Link flee.
  8. End states: leave + offscreen → win/lose scenes + retry/new-seed.
  9. Polish: real sprite atlas, addKaboom/particles, sfx (loadSound/play), shake(), tuning pass on speeds / radii / counts.

Each milestone is independently runnable for playtesting.


13. Tunable constants (initial guesses, config.js)

Constant Value Meaning
TILE 40 px tile size
COLS × ROWS 24 × 14 grid → 960×560 canvas
PLAYER_SPEED 130 px/s player movement
LINK_SPEED ~150 px/s ≈1.15× player
POT_COUNT 58 pots per scene
GRASS_COUNT 815 grass tufts
CHICKEN_COUNT 24 chickens
THROW_TILES 23 pot flight distance
SHARDS_PER_POT 13 shards on shatter
DISTRACT_RADIUS ~4 tiles how far grass/chickens tempt Link
FLEE_TIME ~1.5 s how long Link flees after hitting chicken

First-pass numbers; §12 step 9 tunes them against playtests.


14. Assumptions & open questions

  • "Your PC" = the player-controlled character (Link is the AI). Assumed.
  • Kaplay delivery: CDN ES-module import for a no-build start; can switch to npm create kaplay + Vite for bundling/TypeScript if we want it.
  • Sprites: no Zelda art bundled; starting with emoji/text() and shape placeholders, swappable for a real atlas via loadSprite later.
  • Link leaves when out of pots (max-time fuse as backstop); a fixed round timer is a one-line alternative in config.js.
  • Chicken chase: only the hit chicken chases (simplest); could expand later.
  • Audio is polish (step 9), not core.