Files
MeatSlicer/docs/development.md
T

13 KiB
Raw Blame History

Development Guide

This guide consolidates the durable project knowledge used to develop, test, package, and maintain MeatSlicer. When this guide and the implementation disagree, the implementation and configuration files are the source of truth.

Memory version: 0.5.2

Architecture

MeatSlicer is an endless top-down arena roguelite built with plain JavaScript and HTML5 canvas. It has no browser framework, module system, or bundler.

index.html loads the scripts in js/ as globals in a fixed order:

  1. utils.js
  2. state.js
  3. input.js
  4. sfx.js
  5. sfxbank.js
  6. audio.js
  7. sprites.js
  8. particles.js
  9. weapons.js
  10. bullets.js
  11. pickups.js
  12. items.js
  13. perks.js
  14. enemies.js
  15. bosses.js
  16. rooms.js
  17. player.js
  18. hud.js
  19. help.js
  20. debug.js
  21. main.js

Because the scripts share globals, changing their order can break initialization or runtime dependencies.

Runtime details

  • The canvas uses a fixed 960×640 logical resolution and is CSS-scaled with pixelated image rendering.
  • There is no camera. Rooms use variable G.arena inset bounds for full halls, horizontal or vertical halls, chambers, and pits.
  • Persistence uses localStorage. Current keys include meatslicer_best, the music and SFX volumes, meatslicer_autoperk, meatslicer_pressure_dial, and meatslicer_hud_alpha.
  • powerScore() weights perks at 0.5 and item tiers at 1.0 when scaling enemies.
  • G.pressure is an adaptive difficulty multiplier, clamped from 0.60 to 2.00. The title-screen pressure dial ranges from −10 to +10.
  • procOnHit in js/enemies.js is the shared on-hit payload router used by all weapons.

Cache busting

Asset and script URLs use a ?v=N cache suffix. When shipping changed assets or scripts, bump the value together in:

  • index.html
  • js/sprites.js
  • js/sfxbank.js

Repository layout

Path Purpose
index.html Browser entry point and script load order
js/ Game systems and content definitions
css/ Browser and canvas presentation
assets/ Shipped sprites, animation sheets, UI art, and sound effects
assets/raw/ Generation intermediates; ignored and excluded from packages
mp3-music/ Music and boss tracks
desktop/ Electron main process, preload, and verification harness
tools/ Static server and visual asset-generation tools
test/smoke.js Headless integration and regression suite
docs/ Player, content, release, and developer documentation
build/icon.png Windows application icon

Build, run, and test

Browser

On Windows:

play.bat

This runs the allowlisted static server in tools/serve_game.py and opens http://localhost:8123. The equivalent manual command is:

python tools/serve_game.py --port 8123

Use play-dev.bat or append ?dev=1 to enable the debug console.

Electron

npm install
npm start

Use npm start -- --dev to enable the debug console in Electron.

The lockfile currently resolves Electron 40.10.2 and electron-builder 25.1.8. These versions support the development machine's Node 20 environment; review Node compatibility before upgrading either package.

Tests

Run the complete smoke suite before every commit:

npm test

This is equivalent to node test/smoke.js and must finish with ALL CHECKS PASSED. The suite asserts exact balance values and scaling formulas, so update expectations deliberately when changing game balance.

For a quick JavaScript syntax check:

node --check js/<file>.js

Windows distributable

npm run dist

electron-builder writes a versioned zip and an unpacked application under dist/. The version comes from package.json. dist/ and node_modules/ are ignored; commit package manifests, not generated dependencies or builds.

Electron packaging

The Electron shell serves the unchanged browser game through a privileged app:// protocol.

Package contents

electron-builder.yml uses a strict allowlist for the desktop shell, HTML, CSS, JavaScript, shipped assets, music, documentation, and package.json. assets/raw/** must remain explicitly excluded because it contains large generation intermediates that are not part of the game.

The application deliberately uses asar: false.

Custom protocol requirements

The app:// protocol is registered as standard, secure, fetch-capable, and stream-capable. Streaming is required for looping MP3 audio. The handler must:

  • call decodeURIComponent, because music filenames contain spaces;
  • use path.join rather than path.resolve, because URL pathnames begin with /; and
  • preserve traversal protection before serving a requested path.

Window behavior

  • Borderless fullscreen starts at 1280×832 restore geometry.
  • F11 toggles fullscreen.
  • F12 and Ctrl+Shift+I toggle developer tools.
  • Ctrl+R reloads.
  • The application menu is removed.
  • A single-instance lock prevents duplicate app instances.

winCodeSign extraction issue

electron-builder's winCodeSign archive can fail to extract on Windows when the current account cannot create symlinks. The local workaround is a pre-seeded cache under:

%LOCALAPPDATA%\electron-builder\Cache\winCodeSign\winCodeSign-2.6.0

If the cache must be rebuilt, extract the archive while excluding its darwin entries, or enable an environment that permits symlink creation.

Packaged-app verification

The optional harness in desktop/main.js drives a run, loads the sound bank, captures diagnostics, and exits. In PowerShell:

$env:MS_VERIFY_OUT = "$env:TEMP\meatslicer-verify.json"
Start-Process "dist\win-unpacked\MeatSlicer.exe" -Wait
Get-Content $env:MS_VERIFY_OUT

Set MS_VERIFY_SHOT to a PNG path to capture a screenshot as well. The same variables work with npx electron .. A valid result has an empty log, no SFX failures, and an SFX buffer count matching SfxBank.FILES and the files on disk; the exact count can change as audio is added.

The Windows build is not code-signed, so downloaded copies may trigger a SmartScreen warning. Code signing, installers, automatic updates, and macOS/Linux packaging remain future work.

Visual asset pipeline

The project has two visual asset tiers. AI-generated stills are the production tier; procedural drawings are fallbacks and composition guides.

Production sprites

tools/gen_assets.py generates production stills through OpenRouter's image endpoint and derives animation sheets from them.

python tools/gen_assets.py --list
python tools/gen_assets.py <name-substrings> --quality medium
python tools/gen_assets.py <name-substrings> --force --quality medium

Authentication is read from OPENROUTER_API_KEY in the process environment or the user-only ~/.config/MeatSlicer/.env, outside the served game directory. Never commit credentials.

The generator:

  • attaches assets/raw/guides/style_reference.png as a style reference;
  • requests isolated sprites on #FF00FF backgrounds;
  • flood-keys the border to alpha;
  • crops, centers, resizes, and quantizes output to the shared palette; and
  • records generation metadata in assets/raw/openrouter_manifest.json.

render_sheets() calls render_actor_sheet(name, use_existing=True), ensuring animation sheets are derived from the production still.

Procedural fallback sprites

tools/draw_sprites.py contains procedural fallback drawing functions. It is useful for guides, runtime fallbacks, and derived effects.

Destructive-tool warning: Never run draw_sprites.py without an explicit, narrow name filter. Its main routine writes every matching procedural still and rebuilds sheets with use_existing=False, which can overwrite production AI stills and their derived atlases.

Prefer gen_assets.py for shipped characters.

Actor animation atlases

  • Boss frames are 128×128.
  • Player frames are 96×96.
  • Normal enemy frames are 64×64.
  • Atlases use eight columns and eight compass directions.
  • Cell lookup is offset + direction * frames + frame.
  • Source coordinates are (cell % 8) * frameSize and floor(cell / 8) * frameSize.
  • Actions match ACTOR_ANIMS in js/sprites.js: idle, move, attack, hit, and death.
  • Player legs use a separate eight-frame strip.

To add a production animated character:

  1. Add its Spec to SPECS in tools/gen_assets.py.
  2. Add its name to ACTORS in tools/draw_sprites.py.
  3. Run python tools/gen_assets.py <name> --quality medium.
  4. Register the still and sheet in SPRITE_MANIFEST as required.
  5. Bump the shared cache version before shipping.

Audio pipeline

Sound effects are generated with the ElevenLabs audio tooling. There is no repository-local generation script, so requests should use a per-file manifest with the exact target filename, sound description, and style brief.

Generated files may initially land in the project root, and generated filenames may replace underscores with hyphens. Move and rename each result into assets/sfx/ using the underscore filename expected by the game.

To wire a new sound:

  1. Generate the clip using its exact manifest and style description.
  2. Move and rename it into assets/sfx/.
  3. Register it in SfxBank.FILES in js/sfxbank.js.
  4. Run npm test to verify every registered file exists and is non-empty.
  5. Bump the shared cache version before shipping.

Sfx.sample() and Sfx.ui() can fall back to procedural audio when a sample is missing, but registering a missing file in SfxBank.FILES fails the smoke test.

Versioning and releases

Versions use X.Y.Z with project-specific rules:

  • X is the main version and is changed manually by the project owner only.
  • Y marks a major feature or content expansion.
  • Z marks a fix, minor addition, balance tweak, or adjustment.
  • Components do not roll over automatically at 9; values such as 0.42.677 are valid.

The version lives in package.json and determines the packaged artifact name.

When changing the version:

  1. Update package.json.
  2. Bump the shared ?v=N cache value in index.html, js/sprites.js, and js/sfxbank.js.
  3. Run npm test and commit the source changes.
  4. Run npm run dist.
  5. Verify the packaged executable with MS_VERIFY_OUT.

Memory versioning

The project knowledge ("memories") — this guide and the player-facing documents under docs/ — carries its own X.Y.Z version, tracked independently of the game release version in package.json:

  • X — a major milestone. Changed by the project owner only; a routine update or bump rule never raises it.
  • Y — a major feature push: a large content expansion, a major mechanic, or a significant restructure of the knowledge base.
  • Z — a small addition, a bug fix, or a single new small feature (a new gotcha, a corrected formula, a changed workflow, or the documentation sync that follows a gameplay change).

Components are independent counters and never roll over: 0.999.999 and 0.42.677 are perfectly valid, and bumping Z on 0.999.999 gives 0.999.1000. Only X is exempt from automatic increases — it changes by explicit human edit.

The current version is the **Memory version:** stamp at the top of this guide; that stamp is the single source of truth.

When a change to durable knowledge lands (anywhere under docs/):

  1. Edit the stamp: bump Z for a small addition or fix, Y for a major push, or — for a milestone — the project owner raises X by hand.
  2. Run npm test (docs are not loaded by the game, so the suite is unaffected) and commit with an imperative message naming the bump, e.g. docs: record Iron Lung synergy (memory 0.5.0 -> 0.5.1).
  3. No ?v= cache bump is needed — docs/ is not served through index.html script tags; it only ships inside packages.

Git workflow

Every completed change should be committed:

  1. Run git status and review git diff.
  2. Stage only intended files.
  3. Never commit credentials, local environments, raw generation data, dependencies, or build output.
  4. Run npm test.
  5. Commit with a concise imperative summary matching the repository history.

The primary branch is main, and origin points to the public GitHub repository. The local repository uses the GitHub account's noreply identity for new commits.

Keeping project knowledge current

Architecture, commands, workflows, naming conventions, and costly gotchas should be documented when they become durable project knowledge. Keep one topic per section, record why a constraint exists, and update stale guidance instead of adding contradictory instructions.

Player-facing mechanics belong in the relevant files under docs/. Keep those documents synchronized with gameplay changes. This development guide should be updated whenever the build, packaging, asset, audio, release, or contribution workflow changes.

Keep the **Memory version:** stamp at the top of this guide current: any durable-knowledge change bumps it per ## Memory versioning.