13 KiB
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:
utils.jsstate.jsinput.jssfx.jssfxbank.jsaudio.jssprites.jsparticles.jsweapons.jsbullets.jspickups.jsitems.jsperks.jsenemies.jsbosses.jsrooms.jsplayer.jshud.jshelp.jsdebug.jsmain.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.arenainset bounds for full halls, horizontal or vertical halls, chambers, and pits. - Persistence uses
localStorage. Current keys includemeatslicer_best, the music and SFX volumes,meatslicer_autoperk,meatslicer_pressure_dial, andmeatslicer_hud_alpha. powerScore()weights perks at 0.5 and item tiers at 1.0 when scaling enemies.G.pressureis an adaptive difficulty multiplier, clamped from 0.60 to 2.00. The title-screen pressure dial ranges from −10 to +10.procOnHitinjs/enemies.jsis 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.htmljs/sprites.jsjs/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.joinrather thanpath.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.pngas a style reference; - requests isolated sprites on
#FF00FFbackgrounds; - 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.pywithout an explicit, narrow name filter. Its main routine writes every matching procedural still and rebuilds sheets withuse_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) * frameSizeandfloor(cell / 8) * frameSize. - Actions match
ACTOR_ANIMSinjs/sprites.js: idle, move, attack, hit, and death. - Player legs use a separate eight-frame strip.
To add a production animated character:
- Add its
SpectoSPECSintools/gen_assets.py. - Add its name to
ACTORSintools/draw_sprites.py. - Run
python tools/gen_assets.py <name> --quality medium. - Register the still and sheet in
SPRITE_MANIFESTas required. - 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:
- Generate the clip using its exact manifest and style description.
- Move and rename it into
assets/sfx/. - Register it in
SfxBank.FILESinjs/sfxbank.js. - Run
npm testto verify every registered file exists and is non-empty. - 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:
Xis the main version and is changed manually by the project owner only.Ymarks a major feature or content expansion.Zmarks a fix, minor addition, balance tweak, or adjustment.- Components do not roll over automatically at 9; values such as
0.42.677are valid.
The version lives in package.json and determines the packaged artifact name.
When changing the version:
- Update
package.json. - Bump the shared
?v=Ncache value inindex.html,js/sprites.js, andjs/sfxbank.js. - Run
npm testand commit the source changes. - Run
npm run dist. - 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/):
- Edit the stamp: bump
Zfor a small addition or fix,Yfor a major push, or — for a milestone — the project owner raisesXby hand. - 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). - No
?v=cache bump is needed —docs/is not served throughindex.htmlscript tags; it only ships inside packages.
Git workflow
Every completed change should be committed:
- Run
git statusand reviewgit diff. - Stage only intended files.
- Never commit credentials, local environments, raw generation data, dependencies, or build output.
- Run
npm test. - 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.