Content engine: the design
Stickbash content engine + Studio (design)
Date: 6 Nov 2026. Owner: ENGINE (a lane under basha-21, the LIVE
coordinator). Status: APPROVED by the user, 6 Nov 2026.
All 15 decisions are made (D14 and D15 by the coordinator as technical
calls). The four [call]s stand. The implementation plan is
docs/superpowers/plans/2026-11-06-content-engine.md.
Marks: [user] = the user decided (6 Nov); [call] = my call, open to the user's veto; [ask] = it was in the decision list at the end, and every one is now DECIDED.
Code citations are file:line in the game repo
StickFight-online-train7 (branch
live-v4train7,
StickBrawl/Assets/_Game/Scripts/..., shortened to
Scripts/...) and in this repo
(server/...).
This changes one earlier non-goal: the 28 Oct live-game design said
"Downloadable content packs. Content ships in app updates (§14.4), but
the catalog stays ready for them"
(docs/superpowers/specs/2026-10-28-live-game-design.md:46).
From now on new content is pushed from our server.
Summary for the founder (one page)
What we are building. One engine inside the game that plays new content from our server with no app update: maps, kill films, skins (with everything attached to them) and taunts / emotes. It is a fixed set of building blocks shipped in the app, about 50 that copy what the game does today plus about 61 new ones the game has never had. These include moving and spinning platforms, switches and doors, portals, lasers, saws, crushers, spawners, rising water and lava, weather, new camera moves, new skin materials and reactions, and taunt props. New items are designs that combine those blocks. A wiring layer and a tiny formula set let designs make new behaviour, all as data with no code downloaded.
Who makes content. AI helpers make about 95 % of it, through a command-line Studio with a full rulebook, precise error messages, preview pictures and videos, test matches against bots, templates and a playbook. A Mac Studio app gives people the same tools visually. Freehand drawing and posing come in a later Studio update.
How it reaches players.
- Every item passes automatic checks: every arena, every gore level, every phone tier, frame time within budget, two runs exactly equal, and no bug-looking frames.
- A person approves it.
- It goes to our test phones, then 10 % of players for a day, then everyone.
- Phones download new content at the splash screen with a progress bar. If there's no internet, the game carries on with what it has.
- Online rooms check that everyone has the same content and download anything missing first.
- Any item can be pulled in one tap. Players keep what they bought: it rolls back to its last good version, or hides until fixed.
What stays the same. Every existing map, film, skin and taunt stays exactly as it is. A proof set (3 maps, 6 films, 6 cosmetics, 4 taunts) is rebuilt in the engine only to prove it matches the originals with zero difference. Players keep the originals.
Safety rules built in. No downloaded code, only data (the app-store rules). The formula set is tiny, and one switch can turn it off. Content can never be gorier than the player's level or the app's rating, never give an advantage, and never run slower than the frame budget. Two phones in a room can never silently disagree.
Build order (the user). One engine at a time, each finished before the next: first the map engine, then the animation (kill film) engine, then skins, then taunts / emotes, then the shared shop / pass catalogue and launch polish.
Cost and time. About 174-212 agent lane-days. The honest calendar depends on how many engine lanes run at once (see §10): the map engine is done in about 9-11 weeks on two lanes, and the whole program is launch-ready in about 7-8 months on one lane, 4½-5½ months on two, or 3¾-4½ months on three. Launch waits for all of it: the engines, the Studio and the proof set.
Decided (6 Nov).
| # | Decision | Choice |
|---|---|---|
| D1 | New blocks at launch | ~61 new T1 blocks |
| D2 | Approval | a person approves, enforced by the server |
| D3 | Studio | all four kinds at launch, agent-first; drawing / posing later |
| D4 | Missing pack | download with a progress bar first (the splash and at room join) |
| D5 | When to download | at app open, any network, with a progress bar |
| D6 | Space | 5 MB a pack, 150 MB in total |
| D7 | Exact | identical, zero difference |
| D8 | Proof set | as listed, with the HOODIE |
| D9 | Rollout | test phones → 10 % → everyone |
| D10 | Film motion | from the kill pages |
| D11 | Gore levels | all three authored |
| D12 | Formulas | a tiny bounded set, with a kill switch |
| D13 | Pulled item | players keep it |
| D14 | File hosting | our server behind Cloudflare |
| D15 | Content home | a new private repo, stickbash-content |
1. Intent
We want more content, faster. Skins, maps, kill films, taunts and emotes, along with everything attached to them (a skin's film, its ammo, its sparks), should reach players from our server with no app update [user].
To do that the game gets one engine. It is a fixed set of building blocks that ships inside the app. Every new item is a document that says which blocks to use and how to set them. Packs of these documents, plus any sound files they need, are pushed from our server. The same game, built for the Mac with its editor turned on, is the Studio. That is where we (our team and Claude agents) build content, test it in real matches and publish it [user]. Agents use the same Studio from the command line on the VM, with no screen [user].
What exists today stays exactly as it is: the ~180 kill tracks, the ~155 film scripts, the 32 code-built arenas, the skins and the taunts [user]. A small proof set is rebuilt in the engine only to prove that it can match the originals exactly. Players keep the originals [user]. The shop, pass, rooms and customize screens treat built-in items and engine items the same way [user]. Moving the old content onto the engine can happen later, item by item, behind a switch that can be rolled back [user]. Launch waits for the engine, the Studio and the proof set [user].
2. Goals, non-goals, anti-goals
Goals
- Ship new skins, maps, kill films, taunts and emotes, and everything attached to them, with no app update.
- Make the engine very flexible. Blocks combine freely, every block has wide settings, anything can attach to anything that has an attach point, and any item can have variants (gore level, device tier, mirror, skin family).
- Use one engine and one tool for every kind of content: the same document model, timeline, gate and publish path for maps, films, skins and taunts.
- Make whole classes of online bugs impossible by design rather than
catching them one by one. The Half-float
hashAtbug (§3.5) must be impossible to write. - Bake performance, gore levels and the age rating into the blocks, and check them at publish time. Pushed content can never be slower, gorier or rated higher than the app allows.
- Let an agent take an item from idea to the test channel with no human at a screen. Agents do ~95 % of the authoring, so the agent interface comes first (§6) [user, D3]. A person approves the final publish to players, and the server enforces it [user, D2].
Non-goals
- No player-made content. Creators are us only [user].
- Players keep the existing content. Rebuilding the proof set does not replace it [user].
- No migration of old content before launch. Migration comes later, behind a switch [user].
- No new gameplay rules or weapons through packs. Packs are looks, worlds, films and performances. A new weapon (its damage and handling) still ships in an app update [call].
- No change to the 144 protocol until the engine lands (§5.8).
Anti-goals (things the design must never allow)
- Downloaded executable code of any kind: no scripts, no general expression or scripting language (only the tiny bounded formula set of §5.5a, D12), no plugins, no code inside AssetBundles (Apple 2.5.2 / DPLA 3.3.2, Google Play's Device and Network Abuse policy) [user, hard].
- Two phones in one room that disagree about the content without knowing it.
- A float that comes from content data going onto the wire.
- An item that gives a gameplay advantage. The no-advantage rule of the 3 Nov design (§7 there) applies to every pack.
- A pushed item that is over the age rating, or that shows more gore than the player's chosen level.
- A pushed item that can freeze, stutter or crash a phone. The gate refuses it before it ships, and if one gets out anyway the phone's own budget meter drops it.
3. What exists today (the study)
3.1 Kill films: what is data and what is code
A page kill has two halves:
- The track (data).
Resources/KillTrack/<weapon>_<id>.bytesholds every gore level in one file (Scripts/Core/PageTrack.cs:6-9, formatScripts/Core/PageTrackV2.cs:8-14). It is baked from the HTML kill page bytools/killport/(bake recorder →pk_pack.pytext →pk_pack2.pybinary). It runs at 60 Hz (PageTrack.cs:11). It carries the two bodies' 13 joints per frame, the camera (x, y, width, rotation, shear), the speed curve, the weapon rows, the alive / hide / head-gone / ragdoll flags, the events (rounds, flashes, cutsK, blood, soul, cuesS, fxT), the multicam views, split screens, panels, whip pans, pips, grades, real grades, key lights, warm spots, grain, contact shadows, material looks (gold / frost / char / tint), blood styles, death cracks, text, trails, piece paths, draw order, and the page's full sound (voicesA, bedsB, ducksN, libraryL) (PageTrack.cs:12-120). The v2 file is already integer-quantised: every number isq = value × 10^4and positions sit on a 0.01 page-unit grid (PageTrackV2.cs:11-14). A decoded level costs ~0.2-0.6 MB, and a match's film pool is decoded on a worker thread (PageTrack.cs:201-232). - The film script (code). Each film registers a
PageKill.Deffrom its own file through the auto-registration row (Scripts/Core/PageKills/PageKillRegistry.cs:4-12). The Def has plain fields: key, name, weapon, rarity, arena, wire id, slot, skin family, gap, fx scale, ink-in / out, held-hide windows, voice edits, beds, single camera, stay mode and so on (Scripts/Core/PageKill.cs:44-153). It also has C# hooks:cue,step,draw,drawUnder,drawInset,bodyBack,poseFix,fx,begin/end,drawAfter,drawAfterUnder,room,piecePlace,dryCut,supports,trackOf(PageKill.cs:60-176).
How much of each film is code? I tallied the 155 files in
Scripts/Core/PageKills/:
| What the film does | Files | What it is today |
|---|---|---|
| Motion, camera, clock, rounds, blood, soul, page sound | 155 | track data |
Nothing beyond the Def's fields (e.g. SixShooter.cs, 22
lines) |
9 | pure data |
begin / step (its own state each
step) |
145 / 113 | code |
draw / drawUnder / drawTop
(its own props: slabs, swords, hats, marks) |
140 / 87 / 17 | code |
drawAfter / drawAfterUnder (a prop or mark
that outlives the film) |
44 / 24 | code |
cue (handles or drops a sound cue) |
130 | code |
edits the track's events (tr.ev, e.g. no head pop at
MEDIUM) |
43 | code |
uses a real game weapon as a prop (Weapons.Get() |
44 | code |
voiceEdit (drops or rescales page voices) |
47 | data |
room (a room rule tied to real surfaces) |
13 | code |
skin films (slot = PkSlot.Skin) / mythic |
64 / 1 | data field |
generic across guns (supports /
trackOf) |
11 / 8 | code (small lambdas) |
The custom drawing is all vector shapes from the game's one shape
batch: Line 535 calls, Circle 249,
RawQuad 135, Ellipse 121, Box 46,
Glow 39, SoftEllipse 35, then
BoxAxis, Arc, Tri,
Ring, Star. Colours go through
PageKill.PageColor* (~350 calls), and cuts go through
FinisherCut.*.
Finding 1. A film's motion is already data. What makes 146 of the 155 scripts code is a small, repeating set of jobs: draw shapes tied to joints and time, keep a few timers, react to a cue, patch the track per gore level, put a real weapon in a hand, leave a mark behind. Each of those jobs can be a block.
Where the films run:
FinisherStart → PageKillRotation.Try → FinisherPool.PagePick
(PageKill.cs:25-26). The older KillCine
timeline library (Scripts/Core/KillCine.cs:6-22: actors,
camera language, HUD layers, spot solver) still exists beside it. Gore
has six internal levels, and players pick LOW 0, MEDIUM 2 or EXTREME 5
(Scripts/Core/KillSysGore.cs:5-12). The track holds one
block per level, and a missing level falls back to g5
(PageTrack.cs:172). Film quality tiers are LOW / MED /
HIGH, with a film governor that steps down under load
(Scripts/Core/KillCineQuality.cs:48-58). Sound plays the
page's voices on the film clock through the page library, the gameplay
gun clips and the beds / ducks
(Scripts/Core/PageKillSound.cs:6-13).
3.2 Kill films online
The host picks the film and sends one FilmStart at film
time 0. Every phone plays the same film locally as "a pure function of
the track + time", with a seeded Rng. Gore is local, and the end-state
hash covers only gore-independent state
(Scripts/Core/PageKillNet.cs:7-20). Films are named on the
wire by a u16 wire id from an append-only table
(Scripts/Core/PageKillRotation.cs:775-800). The catalogue
of key @ wire is hashed into
NetProto.ContentHash
(Scripts/Net/NetProto.cs:147-160), together with the
weapons, moves, arenas and items. A mismatch refuses the join with "host
and you run different game builds. Update both"
(Scripts/Net/NetHost.cs:413). Protocol is 144
(NetProto.cs:9).
3.3 Maps
- Geometry is already data-shaped.
ArenaDefholds plats, movers, props, ice, start weapons, spawns, bounds / camera / kill / focus areas, ladders, pads, low-g zones, decor, hatches and racks. It is built by a small DSL (S,O,Floor,Ledge,Stairs,Ramp,Ladder,Pad,Lift,Cart,LowGrav,Hatch,Spot,Deco,Bounds) (Scripts/Level/Arenas.cs:8-170). Hazards on the older arenas are flags with code behind them:lavaTop,wind,rain,laser,pendulumPivot,train(Arenas.cs:52-57). Arenas travel as indices, and ContentHash covers the list (Arenas.cs:209). - Moving pieces, events and art are code. A map with
a
MapScriptbuilds its own pieces (MapPlat: kinematic boxes, one-way or solid, turned, shown / hidden), owns its events and draws itself (Scripts/Level/MapEvents.cs:6-20,:132-187). Events are telegraphed (phase 0 = the warning) and host-authoritative. The host writes each running event's(slot, run, phase, phase time, payload)into the level snapshot, and each event writes its own payload with hand-writtenWrite/Read(e.g.Scripts/Level/MapWreck.cs:341-352,:397-398). The page-90 / 93 maps share a kit: a fixed timeline of cues × the MAP EVENTS pace, props reserved at build time and parked until released, and the small-arena frame (Scripts/Level/MapNewKit.cs:7-17). WRECKING YARD (MapWreck.cs:6-25) is typical. Its geometry is 15 lines of DSL. Its balls, bricks, plank, smash and pallet are ~330 lines of script, event and drawing code. - Map art is code per map
(
Scripts/Render/MapLook*.cs, ~45 files), drawn with the same shape batch. - Map looks are already data with a live overlay.
MapLookConf(Scripts/Render/MapLookConf.cs:9-20) has a shipped default JSON, thenops.maplookfrom the live config, then an event overlay. This is a working pattern the engine copies. - Bot navigation is computed from geometry.
NavGraphbuilds walkable spans and links (walk, jump, fall, drop, ladder, pad, lift) by simulating the fighter's own movement (Scripts/Fighter/Nav.cs:6-11). A data map gets bots for free, as long as its moving pieces declare how bots should treat them. - The map export
(
Scripts/Core/MapExportLab.cs:12-24) writes every playable arena to58k_maps.jsonusing the element vocabulary (~/StickBrawl-v3/v4/feedback/_tools/58k_ELEMENTS.md: FLOOR, WALL, CEILING, PLATFORM, DROP, GAP, STAIRS / SLOPE, PILLAR, OPENING, HANGING_HEAVY, EXPLOSIVE, BREAKABLE_*, SHARP, HOT, ELECTRIC, LIQUID, MOVING, BOUNCY, CLIMBABLE, ...). It is lossy. It writes page units rounded to 2 decimals (MapExportLab.cs:29), and it holds tags rather than behaviour. It is the right vocabulary for the engine's map elements, but a reverse loader of that JSON cannot reproduce a map exactly. The engine's map document must hold the exact geometry, and can export the 58k JSON as a by-product.
3.4 Skins, cosmetics, taunts
- Cosmetics are recipes. Each item is a
CosmeticDefwith a slot (Hat, Face, Body, Aura, Trail, WeaponSkin, KillFx, Emote, WinDance, Colour, Outfit, Hair, Back, Plate, Pose), a rarity, a source, a 1-based wire index per slot (append-only), anartrecipe enum (HatArt.Crown, OutfitArt.Hoodie, ...) and two recipe colours (Scripts/Render/CosmeticsCatalog.cs:13-58). The drawing for each recipe is C# (Render/Cosmetics*.cs). - Premium weapon skins attach things. A premium skin
carries its round-end film, themed ammo, a death burst, hit sparks and a
feed icon (
Scripts/Render/SkinContents.cs:5-14). Today GOLD and ICE are the premium families. - The loadout on the wire is bytes: one u8 per slot,
4 emote ids and a bitmap of skinned weapons
(
Scripts/Render/CosmeticsLoadout.cs:167-185), capped atWiFiProfile.MaxLoadout= 96 bytes (Scripts/Net/WiFiProfile.cs:23). - Taunts are keyframe tracks built by a DSL. Each key
is a time, an ease (
TE), torso / head / drop offsets and hand targets (Scripts/Render/TauntDefs.cs:31-60,TauntRig.cs:15-24). Faces, comic words, props and a voice clip go with them. Taunts are emote ids 22-33, a u8 on the wire, host-decided, withEv.Taunttelling every client which one (Scripts/Render/Taunts.cs:7-17,:78). Gloats turn a taunt near a KO'd body into its own variant (CHAI BREAK sits on the body, LOSER leaves an L that stays). Emotes 0-21 are hand-keyed poses (Scripts/Render/CosmeticsEmotes.cs:5-10).
Finding 2. All of the game's art is SDF vector
shapes from one shader and one dynamic mesh per layer
(Scripts/Render/ShapeBatch.cs:9-13: capsule, rounded box,
arc, triangle, glow, star, ring, soft, ellipse). The only non-code art
is a handful of textures (Resources/Art 744 KB,
ItemArt 1.8 MB) and the sound (Resources/Audio
98 MB, 172 kill-library files). So most new art can be pure
data (shape lists), with no AssetBundle and no shader at all.
Only new sound files, and rare raster art, need bundles.
3.5 Live config, catalogue, server
- The live config is one versioned JSON document. It is cached and
checked whole, applied only when its
veris higher, and read through typed sections with built-in defaults (Scripts/Live/LiveConfig.cs:12-25). Film settings are already one of those sections (Scripts/Live/LiveFilms.cs). - sbapi publishes config versions from the dashboard or CLI. A
rollback re-publishes an old version's content as a new, higher version
(
server/sbapi/config/publish.go:212-214). - The catalogue: the game exports every item it knows
(
Scripts/Live/Season/CatalogExport.cs:11-17, items withsinceVersionfromCatalogMeta). sbapi keeps one catalogue per app version code (deploy/catalogs/catalog-<versionCode>.json) and auto-picks season passes from it (server/sbapi/seasons/catalog.go:1-8). - The Half-float lesson (6 Nov).
FilmStartsendshashAtas a half float (PageKillNet.cs:45,:58). The host sampled the end hash at the unrounded time, the clients at the rounded one. On 48 of 195 tracks they picked different frames, and the end hashes "differed" (fix996026f80: the host now samples at the half value too). The deeper cause is that a float derived from content crossed the wire through a lossy type. §5.7 makes that impossible.
3.6 Platform facts
- Unity
6000.3.26f1(StickBrawl/ProjectSettings/ProjectVersion.txt), IL2CPP with managed stripping (hence the registry rows instead of reflection,PageKillRegistry.cs:9-11). - The Addressables package is not installed. Content
loads through
Resources. The built-in AssetBundle and UnityWebRequestAssetBundle modules are present (Packages/manifest.json:17,36). - Store builds compile the labs out (
SB_STORE,Editor/ProjectSetup.cs:562). The lab system runs a player build headless with-batchmode -scenario <name>(cap.sh). The Studio and the agents' CLI are built on that.
4. The idea in plain words
Think of it as LEGO:
- The bricks (blocks) ship inside the app. Examples: "a floor", "a swinging ball", "a hat drawn from these shapes", "a camera cut", "a slow-motion ramp", "a sound at this moment", "the victim's arm comes off here (EXTREME only)". Each brick has settings with safe limits, a known cost and rules for the gore levels. We can only add new bricks with an app update.
- A design (document) says which bricks to use, how to set them and when things happen. Skins, maps, films and taunts all use the same kind of document.
- A box (pack) holds a few documents plus any new sound files. A pack is sealed: its name is its fingerprint, so two phones with the same pack name have exactly the same contents.
- The Studio is our own game on the Mac with an editor switched on. You open a design, see it in a real match against a bot on any arena and at any gore level, change settings, and press GATE then PUBLISH. Agents do the same on the VM by typing commands.
- The gate is an automatic inspector. It plays the item on every arena and gore level and on every device tier, measures the frame time, checks that two runs agree exactly, and looks for bug-looking frames. Nothing reaches players without passing it.
- The server keeps the boxes. It sends them to the internal test channel first, then to a slice of players, then to everyone. It can pull any box back in one tap.
- The shop, pass, rooms and customize screens see one catalogue. A pushed hat and a built-in hat are just two hats.
- New boxes download when the game opens, with a progress bar on the splash screen, before the menu. If there's no internet, the game carries on with what it has (DECIDED, D4 + D5).
- Online rooms compare the list of boxes everyone has. If someone is missing one, they download it (with a progress bar) before they join (DECIDED, D4).
5. Architecture
STUDIO (Mac, internal build) HEADLESS CLI (VM / M2 / Mac)
edit docs · preview in real match studio.sh validate|compile|preview|gate|pack|publish
\ /
└──── content repo (git: JSON docs + source sounds) ────┘
│ compile (quantise, resolve refs, bake bundles per platform)
▼
PACK (content .sbp, sha256-named · bundles per platform · envelope .sbe: report + signature)
│ GATE (schema, budgets, determinism, perf per tier, arenas,
│ gore + rating, look checks, exact-match for the proof set)
▼
sbapi content registry ── content list (channels, rollout %) + kill list (only grows); human approval for live
│
content.stickbash.com (VM origin, Cloudflare in front, immutable URLs)
▼
PHONE: fetch → verify hash + signature → store → ENGINE RUNTIME (app-shipped blocks)
→ shared catalogue (shop / pass / rooms / customize) → matches (pack-set hash in the join)
5.1 The runtime
The runtime is one C# module inside the app
(Scripts/Engine/):
- Pack store. Packs on disk
(
Application.persistentDataPath/content/), verified on load. The sha256 of the pack's content is its name (§5.8). Its envelope carries an Ed25519 signature by the gate's publish key, whose public half is built into the app. It keeps an LRU of unused packs under the size caps: ≤ 5 MB per pack, ≤ 150 MB in total (DECIDED, D6). Packs that hold an owned item's current revision or its previous live revision are pinned and never evicted (§5.13). - Loader. It reads a pack's compiled documents (a
compact binary, like the v2 track: varints, string table, deflate). It
resolves references and asks the AssetBundle loader for any sound. It
loads off the main thread, the way
PageTrack.DecodeV2and the film pool already do (PageTrack.cs:201-232). - Validator. It runs on the phone again, even though the gate ran. It checks the engine version, that every block id is known to this app, that every setting is in range and every reference resolves, and that budgets are met. A document that fails is dropped whole, with a telemetry line, and the item hides. It never half-plays.
- Interpreters, one per content kind, all driven by
the same timeline core:
- Film: the engine's film blocks run on
today's page-film engine. A
MotionTakeblock is a v2 track, played by the sameFinisher/PageTrackpath. The blocks replace what the per-film hooks do (§5.2 B). So the motion, camera, sound and gore that already pass the port gate are reused, not rewritten. - Map: it builds an
ArenaDeffrom the geometry blocks (the same class the code maps fill), then a genericEngineMapScript : MapScriptthat runs the pieces, hazards, timeline and events from blocks, with a generic payload writer (§5.7). - Cosmetic: a generic recipe drawer. A rig of shapes is attached to body joints or weapon parts, and the premium attachments point at engine or built-in content.
- Performance (taunt / emote / win dance / gloat): a
TauntTrackbuilt from keyframe data, plus face, words, props and clip.
- Film: the engine's film blocks run on
today's page-film engine. A
- Budget meter. Every block reports its cost each
frame (shapes, particles, voices, colliders, insets). If a document is
over its tier's budget at runtime (which the gate should have stopped),
the meter drops its optional
lookparts in a fixed order. This is the same idea asFilmQ's governor, now per block. It never drops or changes asimpart, since that would split host and clients. Asimoverrun is only reported in telemetry, and the gate is what prevents it. - Ownership and saves use the engine item's
string id (
eng.hat.lantern), never a wire index, so a save survives any pack revision. The u8 slot indexes stay for built-ins only.
5.2 The block library
A block is an app-shipped C# class with a stable id
(film.prop, map.pendulum, ...), a typed
parameter schema (every number has a unit, a grid and a range), a
cost formula, a tier rule (what it
drops at LOW / MED), a gore rule (which levels it may
appear at, and what it becomes below that level), a determinism
class (sim: changes the match state,
host-authoritative, hashed; look: drawing / sound only, may
differ per phone) and its attach points. The library
has a version number (ENGINE_API). It only ever grows:
block ids and parameter ids are append-only, a removed block keeps its
id, and a parameter's meaning never changes (§5.8).
The families below cover everything the study found in today's content. Every row names the existing code it wraps, so the first version is mostly a wrapper around tested code, not new behaviour.
A. Common blocks (every kind of content)
| Block | Parameters | Wraps today |
|---|---|---|
shape |
kind (line, circle, ellipse, soft ellipse, box, rounded box, quad, arc, tri, ring, star, glow), size, stroke, fill, outline, colour (a palette slot or RGBA), layer (under / fighters / over / top / inset-only), alpha | ShapeBatch |
rig |
a tree of shapes / rigs with local transforms; an anchor (a body joint 0-12, a weapon part, a world point, a map element, a film piece, the camera); flip with facing | per-film Draw code |
driver |
drives one parameter of one block over time: a keyframe curve (time
→ value, eases from the TE set + cubic Bézier),
follow an anchor, between two anchors at u,
orbit, seeded noise (amplitude, rate), step
list, bounded repeat (n ≤ 64) |
the hand-written math in step / draw |
palette |
named colour slots; page colour mapping
(PageKill.PageColor*), gore colour, skin-theme
colour (Gold / Ice main / light / dark) |
PageKill.PageColor, SkinPremium.Main |
look |
a body or prop material: tint + k, gold, frost, char, outline ink, rim / back light, gloss; spread from a joint over time | track O look, Def.lookInk,
bodyBack |
text |
a comic word or glyph: text (from the pack's string table), style, burst colour, size, rotation, anchor, life | PageKill.Glyph, TWord |
sound |
a voice (library id or pack clip, vol, rate, pan, low / high pass, offset, dur, fades, loop, bus), a bed, a music-kit chunk (bed / pulse / drum / riser / hit), a duck / hush | PageTrack.Voice / Bed, KillBed, the music
kit |
fx |
an emitter from the engine's fixed presets (dust, sparks, debris, coins, leaves, confetti, snow, shards, flakes, smoke, fire, glow, steam, ink), count, spread, life, colour; physics drop with bounce / rest | KillFx, the track's T fx |
grade |
preset (CINEMA, SLOWMO_COLD, NIGHT, IMPACT_SPLIT), filter ops (sat, contrast, bright, hue, gray, sepia), tint, vignette, grain, key light, warm spot, light shaft + projected shadow | track O real / grade / grain / warm, COLD-BLOODED's
shaft |
camera |
shot (centre, width, roll), follow body / joint, cut, whip, shake, punch, zoom; insets / panels / split (master flag, slide side, delay, gutter); single-camera switch | track camera + views, Def.singleCam |
clock |
speed curve keys, freeze, hit-stop, slow-mo sound pitch / low-pass rule | track sT / spd, TimeCtl |
rng |
a seeded choice among variants (weights); the seed is the instance's own, never the global Rng | the film Rng |
counter |
small integers / flags per instance (set, add, compare); part of the logic graph's ≤ 32 state nodes (§5.5) | the films' static state |
B. Film blocks
| Block | Parameters | Wraps today |
|---|---|---|
film.take |
a v2 track (per gore level) imported from a kill page bake or recorded in the Studio; its length in ticks | PageTrack |
film.cast |
roles (killer, victim); weapon binding: its own weapon, a gun class (Compact / Long / Heavy / Flame / Explosive) with a track per class, any weapon (mythic); rotation slot (round-end, skin family, mythic); rarity; rotation weight | Def.weapon / supports / trackOf / slot / skin / anyWeapon,
PageGunMap |
film.fit |
gap range, start melt, stay mode + aim fix, floor width, headroom, a needed surface (wall / ceiling / ledge / drop by element kind, with a shift range), realm windows, last-resort kind | Def.minGap / maxGap / stay / room / realmScene / lastResort |
film.weaponprop |
a real game weapon (by id, or "the killer's", "the victim's") as a prop: in a hand (body, hand, grip offset), flying on a path, dropped (lands flat, stays), ink-in / ink-out, held-part hide windows | inkIn / inkOut / heldHide, the 44
Weapons.Get props |
film.prop |
a rig that appears / disappears at ticks, with states
(whole, cracked, holed, broken) switched by triggers, cast from ink,
collides with the floor, stays after the film for N s |
DRIP's slab, THE X's hat, the ink swords |
film.mark |
a floor / wall decal (shape rig) placed at an anchor at a tick, kept after the film for N s or until the next film | THE X's wall X, DRIP's drip line, drawAfterUnder |
film.cut |
a FinisherCut part at a tick (per gore level), sealed (no blood, cap colour, unseal tick), piece path shape / rest / flat / removed, piece tint | track K,
dryCut / sealColor / unsealScene / piecePlace / piecesFlat / piecesGone |
film.blood |
style switches (main / dark colour), per-tier caps for drops / splats, keep red | O bloodstyle,
lowDropCap / lowSplatCap |
film.soul |
strength, shown-while-hidden | soulAlpha / soulWhenHidden |
film.trackedit |
a declarative patch on the take per gore level: clear head-gone frames, remove events by kind + tag + time window, drop / rescale voices, clear a hide window, add a cut | the 43 tr.ev edits, voiceEdit |
film.posefix |
an IK reach of one limb to an anchor over a window | poseFix |
film.order |
draw-order windows (killer over, victim over, head over, inset over) | killerOver / victimOver / headOver / insetOver |
film.handoff |
the end state: the victim's death pose source, what stays, the GLOAT window | Finisher end, the GLOAT pre-select |
C. Map blocks
| Block | Parameters | Wraps today |
|---|---|---|
map.frame |
bounds, closed / open, follow / fit-all, size badge, camera / kill / focus areas, sky ceiling, players 2-8 | ArenaDef.Bounds and the area fields |
map.solid |
floor, box, ledge (one-way), slope / stairs (filled), shell; look id; material (turf, stone, wood, metal, ...); ice; conveyor speed | PlatDef, the DSL |
map.traverse |
ladder, jump pad (launch vector), lift (stops, period, dwell, cable top), cart, low-g zone (gravity) | LadderDef / PadDef / MoverDef / ZoneDef |
map.liquid |
water (depth, currents, vents), lava (top, damage, bounce), sea out-line | MapWater, lavaTop |
map.spawns |
spawn points, extra spawns (5-8 players), start weapons, racks
(Spot), hatches, drop range |
SpawnsM / Extra / Spot / Hatch / startWeapons |
map.prop |
crate / barrel / static crate; reserved (parked until a cue releases it) | PropDef, MapReserve |
map.piece |
a kinematic platform (MapPlat): size, one-way, surface;
motion = path (points, period, dwell), pivot (angle
curve, or weight-driven seesaw with spring / damping / caps),
breakable (hits to break, debris, rebuild cue),
crumble (stand time → shake → drop → back after N s); bot nav
hint (walk / avoid / ride) |
MapPlat, Seesaw / Crumble / Wreck code |
map.hazard |
pendulum (pivot, length, radius, the castle swing curve
Level.PendAngleAt, phase, solid), kill zone (pit /
grinder / spikes / sea), damage area (damage per touch, burn,
bounce), push (wind gusts, current), magnet (pull
targets by MapMetal, strength); every hazard follows the
room's HAZARDS switch |
Level pendulum, lava, wind, MapMetal |
map.timeline |
cues at fight time (ticks) × the MAP EVENTS pace (NORMAL 1, CALM 1.5, CHAOS 0.65), calm-skip / chaos-only flags, quiet-stretch hold, loop | MapTimeline |
map.event |
a named event: warning (beacons, banner text, sound, length), then phases of actions on pieces / hazards / props (move, drop, swing, rebuild, release, show / hide, set a counter), each phase with a length; declared payload fields (ints) | MapEvent + Write / Read |
map.art |
backdrop layers (parallax), decor kinds (DK.*), signs,
theme palette + surface styles; art rigs on pieces |
Theme, MapLook*.cs,
DecorDef |
map.look |
dim, saturation, locked night 85-100 (same shape as
ops.maplook) |
MapLookConf |
map.elements |
the 58k element tags (kind, tier, hazard, uses) for every element,
used by film.fit and exported to the kill pages |
KillSysMapData, MapExportLab |
Bot navigation is not a block. The runtime builds
NavGraph from the solids and traverse blocks, exactly as
for code maps, and pieces add their nav hint.
D. Cosmetic blocks
| Block | Parameters | Wraps today |
|---|---|---|
cos.item |
slot (any CosmeticSlot), rarity, source, name,
description, showcase colour, set, hidden, film variant of |
CosmeticDef |
cos.art |
a rig attached to a body joint / the head / the hands /
the back, or a weapon's parts (per weapon: part index → recolour,
overlay rig, glint) |
HatArt / OutfitArt / ... recipes,
CosmeticsPlain |
cos.premium |
the attachments: film (a ref to an engine film or a built-in key), ammo theme (round shape rig, trail, impact fx), hit sparks (fx preset + palette), death burst (built-in skin-kill family or engine film), feed icon (rig) | SkinContents |
cos.filmrule |
how the item shows in films / previews (mode, max count, gore) | CosmeticFilmRule |
E. Performance blocks (taunts, emotes, win dances, gloats)
| Block | Parameters | Wraps today |
|---|---|---|
perf.pose |
keys: tick, ease (TE), torso / head / drop / hip
offsets, hand / foot targets in TF frames, elbow / knee
flips |
TauntRig.TB |
perf.face |
a TFace per window |
TauntFaces |
perf.words |
TWord rows |
TauntWords |
perf.prop |
attached rigs with windows (the L sticker, the chai cup) | TauntProps |
perf.clip |
a voice clip (built-in id or pack clip), start tick | TauntAudio |
perf.gloat |
the variant played near a KO'd body: approach (walk to range), sit-on / stand-over / point, a mark that stays | TauntGloats |
perf.kind |
emote (hands-only, wheel), taunt (full-body), win dance | TauntKind |
The tables above wrap today's content. They are the floor, not the launch library. A block that isn't in the app can't be pushed later without an app update, so the launch library also carries a broad set of blocks the game doesn't have yet (§7, "NEW BLOCKS BEYOND TODAY'S GAME") [user, 6 Nov].
How flexibility grows. Blocks compose: any rig on any anchor, any driver on any number, any variant on any setting, and any logic-graph wiring of sources to sinks (§5.5). New behaviour comes from new combinations, with no new code. When a design needs something no block can say, we add a block or a parameter in the next app update (§5.8). Packs that need it are offered only to apps that have it. That is a normal app update, which the store rules allow, and it keeps every pack declarative.
5.3 The content document model
Every item of every kind is one document with the same envelope. The authoring form is JSON in the content repo (diff-friendly, agents can write it). The compiled form inside a pack is binary.
{
"doc": "film", // film | map | cosmetic | performance | (shared) rig | palette | soundset
"id": "eng.smg.night-shift", // stable, namespaced; never reused
"rev": 3, // author's revision (the pack hash is the real identity)
"engine": 1, // the minimum ENGINE_API this doc needs
"name": "NIGHT SHIFT", "rarity": "EPIC",
"rating": { "gore": [0, 2, 5], "flags": [] }, // gore levels it authors; content flags (§5.10)
"uses": ["eng.pack.core-props/rig.ink-sword", "builtin:weapon/smg"], // its references, resolved at compile time
"variants": { "gore": {...}, "tier": {...}, "mirror": true },
"blocks": [ { "b": "film.take", "id": "take", "p": { "track": "take:night-shift" } } // a pack-local ref, never a file path,
{ "b": "film.prop", "id": "slab", "p": {...}, "states": {...} } ],
"timeline": [ { "at": 54, "do": "slab.show" }, { "on": "cue:shot3", "do": "slab.state:holed" } ],
"studio": { "notes": "...", "thumb": {...} } // stripped from the compiled pack
}Rules:
- Ids are namespaced and stable. Engine items are
eng.<kind-or-weapon>.<name>. Built-ins keep their ids (revolver.six-shooter,hat.crown,wreck), and a document refers to them asbuiltin:<registry>/<id>. No engine id can equal a built-in id, and the catalogue refuses a clash. - A document is self-describing and fully resolved at compile time. The compiler turns every time into ticks, every length into millimetres, every angle into 1/65536 turns and every colour into bytes. It inlines palette refs, checks every reference, and records the doc's dependency list (other packs by hash, built-ins by id).
- Shared documents (rig, palette, soundset) let a whole family reuse one asset: an ink-sword rig, the Gold theme palette, a music-kit soundset. They live in library packs.
- Variants are patches on settings, not copies. Per gore level (what changes at LOW / MEDIUM / EXTREME), per tier (what LOW drops), mirror (left / right). The gate plays every variant.
5.4 Attachments and composition
- Attach points are a fixed, app-shipped list: the 13
body joints (
PageTrack.cs:135), the head disc, the weapon's parts (WeaponDef.parts), the muzzle, a film piece (FinisherCut part), a world point, a map element (by kind and id), the camera / screen. Every rig, prop, mark, fx and text names one. - Composition by reference. A premium skin's
cos.premium.filmpoints at an engine film document or a built-in film key. A film'sfilm.propuses a rig from a library pack. A map'smap.artuses a palette document. References resolve at compile time to(pack hash, local index)orbuiltin:<id>. A pack may reference only built-ins, itself, and the packs listed in its manifest'sdeps(each pinned by hash). No cycles, no "latest": a reference never floats to a newer revision. - What a skin can carry (the user's example, "for skin we can attach animation"): the look (art + palette), a round-end film (engine or built-in), themed ammo, hit sparks, a death burst, a feed icon, and its film rule. Later blocks can add more (a kill sound set, an equip animation) without changing any pack already out.
- Overrides are explicit. A document may override a built-in only through the migration switch (§5.14), never by reusing its id.
5.5 Timelines, triggers and the logic layer
One timeline core and one logic layer serve every kind of content. The user asked (6 Nov) for "additional things too which is not in game now (like for example additional elements or logics or things for maps etc)" [user]. Fixed blocks alone can't express new behaviour, so it has to come from combining them. The logic layer is how blocks combine, and it stays data.
Time. Time is an integer tick. Films use film ticks
(60 Hz frames of the take, the same grid as PageTrack).
Maps use fight ticks (the fixed step). Performances use performance
ticks. No content time is ever a float.
The simple form: a timeline + rule list. Most films, skins and taunts need only this.
- Triggers (when):
at <tick>,window <t0> <t1>,on cue:<name>(a trackScue or an event tag),on game:<event>from a fixed list (the gameplay-event sources of the logic graph below),when <condition>,gore <set>,tier <set>. A trigger may also require a state (piece.slab == holed). - Actions (what): show / hide, set a state, start a driver, emit fx, play a sound, place a mark, cut, set / add a counter, release a reserved prop, start a map event phase, camera / clock / grade moves. Each action belongs to a block and is app code.
The full form: the logic graph. Maps and complex films use this. It works like wiring switches, timers and lamps on a board, and the Studio shows it as a node graph:
- Nodes come from a fixed, app-shipped set in three
groups:
- Sources: tick / timer, zone (enter, leave, count), plate, proximity, switch, score, alive count, round phase, a seeded random, a counter, a piece or hazard state, a film cue, a gameplay event.
- Logic: AND, OR, NOT, XOR, compare-with-a-constant, edge (rising / falling), latch, toggle, delay n ticks, pulse n ticks, sequence, select (seeded random / round-robin), counter (add, sub, clamp, wrap), timer (one-shot / repeat), once, cooldown.
- Sinks: the actions of blocks (§7).
- Values on the wires are booleans and small integers (i16). There are no floats, no strings and no lists.
- Evaluation: one pass per tick, in an order the compiler fixes (topological). The same one-pass rule holds for the simple form. An action takes effect for triggers on the next tick, never in the same one, so a counter that re-fires its own trigger advances at most once per tick. Counters are i16 and saturate (no wrap unless a wrap node says so). A wire may loop back only through a state node (delay, latch, counter, timer, toggle), and that node reads the previous tick's value. So there are no instant feedback loops, and a tick always finishes.
- Bounds per document (checked by the compiler and again on the phone): ≤ 256 nodes, ≤ 1024 wires, ≤ 32 counters / timers, ≤ 64 sinks firing per tick, and a fixed logic-state size (≤ 64 bytes, the size of its slot in the level snapshot). Every node costs O(1) per tick, so the worst-case cost is known before the item ships (§7 budgets).
- Determinism:
simlogic runs on the host only. Its packed state rides the level snapshot through the generic writer (§5.7 rule 4). Clients pose everything from that state, andlook-only logic (a skin's glow on hit) runs on each phone on its own. Randomness comes from seeded streams only (§5.7 rule 5,rng.stream§7.6). - Why this is not a scripting language: it has no instructions, no named variables, no unbounded loops, no functions and no way to reach any app API except the fixed sinks. The state space is a fixed-size finite state machine, laid out by the compiler. This is how the engine stays data, not a scripting language, which the store rules require [user, hard]. The one exception is the tiny formula set (§5.5a, DECIDED D12): a bounded arithmetic value that may stand in for a constant in a parameter slot. It adds no control flow.
State machines for pieces and props. These are a finite set of named states with timed or triggered transitions, which the logic graph can drive. The Wrecking Yard plank (up → smashed → rebuilt) and DRIP's slab (cast → whole → holed) work this way.
5.5a The tiny formula set (DECIDED, D12, 6 Nov)
The user chose (B), overriding the recommendation, because it makes
AI-made content more flexible [user]. A
formula is a bounded arithmetic value that may replace
a constant in a parameter slot that allows it. Example:
an aura's glow =
clamp(lerp(0.2, 1.0, speed / 6m/s), 0, 1).
Where a formula may go. Every parameter in a block's schema declares
expr: none | look | sim(§6.1).none: a constant only. This covers all ids, refs, enums, counts, sizes of fixed tables, tick positions of timeline keys, gore-ladder settings and every budget cap.look: a formula allowed on a drawing / sound parameter: colour channels, alpha, size, glow, rate, pitch, volume, a driver's amplitude.sim: a formula allowed on asimparameter of a map block: a mover's speed, a field's strength, a level's rise rate, a hazard's on / off period, a spawner's interval.- Film online state and every gore / rating / budget setting are
none.
Operations (fixed):
add sub mul div(div by zero = 0),min max clamp abs,lerp(a, b, t)(t clamped 0..1),step(edge, x)(x ≥ edge → 1 else 0). Literals carry units (6m/s,1.2s), and the compiler checks the units agree.Inputs (named, read-only signals, fixed per block family and listed in the schema):
- time: fight time, film time, time since the item started, time since its last trigger;
- the wearer / owner's own state (cosmetics, performances): speed, health, airborne, facing;
- the map's state (maps): alive count, score, round phase, the logic graph's counter and timer values;
- distance: between two of the document's own anchors (e.g. a mover and its pivot), never to another player;
- gore level and device tier (read-only; the gore ladder still clamps every gore block).
No-advantage rule: a cosmetic formula may read only its own wearer's state and time, never anything about another player (no "glow when an enemy is near").
What a formula can't do: no loops, no recursion, no variables, no function definitions, no strings, no conditionals beyond
step/min/max/clamp, no writes. A formula only ever produces one number for its slot.Size and parsing. At most 32 nodes per formula. The compiler parses the text to an AST at publish time, validates it (ops, inputs allowed for that slot, units, node count) and stores it as a compact prefix array. The phone validates the AST again on load. A bad formula drops the whole document, like any other invalid value.
Determinism.
simformulas evaluate in fixed point (Q16.16 in int32, saturating;divrounds toward zero;lerp/stepin fixed point), on the host only. Their results feed replicated state (§5.7 rule 3), so phones never compare them and gate runs repeat exactly.lookformulas evaluate in float on each phone and never enter a hash.- Inputs are read at a fixed point in the tick (after the logic graph), so evaluation order is fixed.
Budget.
- Per document: ≤ 64 formulas.
- Per frame on the LOW reference phone: ≤ 2,048 formula nodes
evaluated for
look, and ≤ 1,024 per fixed step forsim(≈ 20 µs each). - The static cost is in the item's budget sum, and the runtime meter counts real evaluations.
Server kill switch.
- The content list carries one flag,
formulas: on | off. When it is off, every formula slot uses its declared fallback constant. The schema makes the fallback mandatory ({ "expr": "…", "else": 0.6 }), and the gate renders and plays every item with formulas off too, so the fallback must also look and play right. - The flag is read at match start and goes into the room's pack-set hash, so a room never mixes the two modes mid-match.
- The content list carries one flag,
App-review risk (note). A downloaded formula is closer to "code" than plain numbers. Apple 2.5.2 forbids downloaded code that changes features, and Google's policy is similar. The mitigations:
- the set is a fixed, tiny arithmetic language over a fixed list of inputs, filling only a declared number slot; it can't call any API, change control flow, loop or keep state (it is like a spreadsheet cell, not a script);
- formulas are written by us only, gated and signed;
- the review notes describe them plainly;
- the kill switch turns them all off at once if a reviewer or a policy change asks.
Residual risk: small, but higher than option (A). If a review objects, we ship with
formulas: offand every item still works on its fallbacks.
5.6 Gore and the age rating
- Gore lives in the blocks, not in the packs. Only
app-shipped blocks can cut, bleed, pop a head or show anatomy, and each
one follows the level ladder of
KillSysGore(MEDIUM: no limbs off, LOW: no blood;KillSysGore.cs:5-12). A pack can choose less gore than a block allows, never more. No pack can add a new kind of gore. The rating ceiling is the app's. - Every film authors its three player levels (LOW / MEDIUM / EXTREME), as the takes do today (DECIDED, D11).
- A film's online state is gore-independent. That
state is the killer's pose, the victim's death state (root, facing, down
/ gone), the end tick and the match result. Cuts, pieces, blood and
wounds are
look(they follow the gore level) and are never in the online hash. - Same length at every level (new films) [call]. A
new engine film must have the same tick length and the same online state
at all three levels, and the gate refuses it otherwise. Built-in takes
do differ per level today:
hashAtis "the shortest gore level's length" (PageKillNet.cs:30, the W11 ladderf630a3197). Sofilm.takehas a legacy ladder mode (end at the longest level, hash the killer at the shortest), used by the proof films and any imported legacy take. - Content flags: each document declares a small,
fixed set of content flags (blood colour, dismemberment, fire on bodies,
horror realm, rude gesture, text in a pack's string table). The gate
checks them against the app's
AgeRulesceiling and against a word list for the string table. Words, signs and names are pack strings, so they are checked like the existingNameRules.
5.7 Determinism by construction (the Half-float class made impossible)
These rules make the 6 Nov class of bug impossible to write:
- Integer grids at authoring time. The compiler quantises every content number once, onto a declared grid: ticks, mm, 1/65536 turns, and 1e-4 units for dimensionless numbers (as v2 tracks already do). The runtime stores these integers and converts to float only at the very end, for drawing and physics input. Any "sample time" (an end-hash tick, a film length, a cue time) is an integer that every phone reads from the same pack bytes.
- The wire carries no content floats. Engine content
travels in new messages written by a typed writer,
EngineWriter. It has onlyContentRef,Tick(u16 / u32),Seed(u16 / u32), small ints, enums and the snapshot's existing quantised position type (Pos: int16 mm). It has noF32and noHalfmethod, so a lossy float can't be added by mistake.- An engine film is started by
EngineFilmStart, notFilmStart. It carries the film ref, the seed, the fighters, their feet and floor asPos(mm), the room shift in mm, the start, length and hash points as ticks, and the host's start as a snapshot tick. - Built-in films keep
FilmStartexactly as it is (PageKillNet.cs:43-58, Half and F32 fields included). The engine does not touch legacy content, so that path keeps today's rules and today's fix.
- An engine film is started by
- Two kinds of online state. The rule for each is
different:
- Replicated state is computed on the host only and
sent quantised: map pieces, hazards, the logic graph, spawns, props that
collide, event phases. Phones never need to agree on how it was
computed, only on the bytes they receive. Each phone poses its own
picture from those bytes with a pure function
(state ints, tick), the way WRECKING YARD's balls follow the clock (MapWreck.cs:23-25). Float maths inside that pose function (the pendulum's trig inLevel.PendAngleAt, Box2D contacts) stays on the phone and is never compared. - Recomputed-and-hashed state is what every phone
works out for itself and then compares: today only a film's online state
(§5.6). It must be integer, or quantised to the wire grid before it is
hashed. Anything computed in floats (IK in
film.posefix, a film prop's floor bounce) islookand never enters the hash. lookblocks (gore, tier extras, fx, sound, grade) may differ per phone and never enter a hash.
- Replicated state is computed on the host only and
sent quantised: map pieces, hazards, the logic graph, spawns, props that
collide, event phases. Phones never need to agree on how it was
computed, only on the bytes they receive. Each phone poses its own
picture from those bytes with a pure function
- A generic event payload. A
map.eventand the logic graph declare their state fields (ints with ranges). The runtime writes and reads them generically into the level snapshot (MapEventSnap), so there is no hand-writtenWrite/Readthat can drift between host and client. - Seeded randomness only. Random draws come from
named streams seeded by a stable hash (FNV-1a over the
id bytes, never
string.GetHashCode) of the round or film seed, the content id, the instance and the stream id. The gate records each stream's draw count per tick.- For the proof maps, a director-compat stream
reproduces today's shared map-director Rng and its seed formula exactly
(
MapEvents.cs:192,:314). PALLET's x (MapWreck.cs:385) and Belt Line's crate x (MapBeltLine.cs:361) draw from it in today's order. - New content never uses the compat stream.
- For the proof maps, a director-compat stream
reproduces today's shared map-director Rng and its seed formula exactly
(
- Golden determinism runs at the gate, in three
steps:
- Every document plays twice in one process; the per-tick state hash must match on every tick, not only at the end.
- It plays again on the other desktop build (Mac vs Linux headless).
- Before it can leave the test channel, it plays on the reference phones (one iOS ARM, one Android ARM), because a Mac / Linux pair can't show what ARM maths does.
- A mismatch in recomputed-and-hashed state fails the gate and names the first tick and block.
- The end hash uses the same rules. The film end hash samples at the doc's declared end tick, which is an integer from the pack. Its value is the same at every gore level (§5.6), or the legacy ladder rule applies for imported legacy takes.
5.8 Versioning, pack hashes, compatibility
ENGINE_APIis an integer in the app (1 at launch). Each document declares theengineit needs, and an app shows and plays only documents whoseengine≤ its own. Others stay downloaded and silent until the app updates.- Block and parameter ids are append-only. A parameter's meaning never changes, and a new behaviour gets a new parameter or a new block.
- Any change to how a
simblock behaves, even a bug fix, bumpsENGINE_SIM, a second integer. - The join carries both numbers. Engine maps and engine films are
offered in a room only when every member has the same
ENGINE_SIM. Built-in content, and the members' cosmetics (which arelookonly), are not affected.
A pack is three parts:
- the content (
.sbp): the compiled docs, plus a manifest with the pack id, revision,engine,ENGINE_SIMfloor, deps by hash, the docs' ids and kinds, and the hash of each platform bundle; - the bundles: one AssetBundle per platform (iOS, Android, Mac, Linux for the VM gate), present when the pack has sounds or raster art;
- the envelope (
.sbe): the gate report, the Unity version and texture formats the bundles were built with, the approval record, and the Ed25519 signature over the content hash and the report. - The pack's identity is the sha256 of the content alone. Nothing in the content refers to the envelope, so the hash isn't circular.
- Files are immutable: a fix is a new revision, which is a new hash.
- the content (
The room content set. Packs fall into two groups in a room:
- What the room can play (engine maps and films the
host may pick): only packs that every member has, or
can fetch and run (its
engine/ENGINE_SIM). The host builds this list at match setup, sorted by hash. Its 64-bit hash goes into the hello / join / relay create next toContentHash(NetHost.cs:275,OtSession.cs:228-229). The relay server stores and compares it like ContentHash, which is a relay change in Stage 2 (Task M.8). - What members wear (their cosmetics and taunts):
each member announces the pack hashes of its equipped engine items once,
in its profile block. Others fetch those packs in the background, and
until a pack is ready (or if their app is too old for it), they draw
that slot as the built-in default. Cosmetics are
lookonly, so this never splits the match. - A joiner missing a playable pack downloads it with a progress bar, then joins (DECIDED, D4).
- Wi-Fi rooms with no internet: the playable set shrinks to what every member already has, and cosmetics use the default fallback.
- Staged rollout (§5.13) only controls what the shop offers. Any approved pack hash can always be downloaded, so a joiner outside the rollout bucket can still fetch what the room plays or what a friend wears.
- What the room can play (engine maps and films the
host may pick): only packs that every member has, or
can fetch and run (its
Content refs on the wire are
ContentRef = (u8 index in the room's table, u16 local index). The room's table is the playable set plus the announced cosmetic packs, built identically on every phone.The loadout cap [call]. Today v2 worst case is 127 B against
WiFiProfile.MaxLoadout= 96 and relies on dropping skins (CosmeticsLoadout.cs:183-185). At protocol 145:- the cap rises to 192 B;
- each engine slot costs 3 B (
ContentRef); - when a loadout is still over the cap, items are dropped in this order: PLAIN skins, then engine weapon skins, then other skins, then engine body slots;
- the header and the built-in slots always go, so nobody is shown undressed.
Protocol. The engine needs one bump, 144 → 145, when it lands. It covers:
- the pack-set hash and
ENGINE_API/ENGINE_SIMin the join and the relay; EngineFilmStart;- a
ContentRefin the taunt event; - the loadout extension and its new cap;
- the arena field for an engine map;
- the generic map / logic snapshot block.
NfOldRoomProtocolalready shows how an old build is kept out of a new room (Scripts/Net/NetFilmLab.cs:20). There are no players yet, so the bump costs nothing if it lands before launch [call].- the pack-set hash and
PageKillWire stays as it is. It is append-only and covers built-in films only (
PageKillRotation.cs:775-800). Engine films never take a wire id: they travel asContentRef. Built-in ids, emote ids 0-33 and cosmetic slot indices are untouched.
5.9 Asset delivery (sound and the rare raster)
- Vector first. Art is shape data in the documents (Finding 2). Most packs have no bundle and are a few KB to a few hundred KB.
- AssetBundles for new sound and raster art
[user]. Only the Unity Editor can
build bundles (
BuildPipeline), not a player. Sostudio.sh packhands that step to a bundle lane: the Unity Editor (6000.3.26f1, the same version as the app) in batch mode, with the iOS, Android, Mac and Linux modules installed. It runs on the Mac or the M2 first and on the VM once its Editor is set up (Stage 2, Task M.9). It builds one bundle per platform: iOS, Android and Mac for players, Linux for the VM gate. The envelope records the Unity version and the texture formats (ASTC for iOS / Android, DXT / BC for desktop). They hold onlyAudioClipandTexture2D(andFontif ever needed). Bundles may never contain aMonoScript, a shader, a material with a custom shader, or a prefab with components. The gate opens each bundle and refuses any other type. Textures draw with the game's shipped shaders only, so there are no new shader variants to precompile. We use the built-in AssetBundle API with our own manifest rather than adding the Addressables package [call]: the pack manifest already does Addressables' cataloguing, and we avoid its stripping, catalogue and build-time cost. - Clips use the game's existing import settings (Vorbis, the kill
library's sample rate), so a pushed sound behaves like
Resources/Audio/Kill. - Size caps (DECIDED, D6): ≤ 5 MB per pack and ≤ 150 MB in total, with LRU eviction of unused packs; owned items' packs are always kept. The gate refuses a pack over 5 MB on any platform.
5.10 The Studio (Mac app)
The Studio is the internal Mac build of the same game
(non-SB_STORE, like the lab builds) started with
-studio. It is never in a store build. Its screens:
- Library: every document in the content repo and every built-in item (read-only, as a reference and a source to copy settings from). Filters by kind, weapon, map and status (draft, gated, live).
- Editor: the real game view plus a timeline (tracks per block, keys, triggers, a scrubber at tick resolution, gore / tier / mirror toggles) an inspector (every parameter with its range, unit and cost) and a logic-graph view (nodes and wires, with live signal values while the item plays). Undo / redo. Live reload: saving the JSON re-plays at once.
- Try it: drop the item into a real practice match against a bot on any arena (films: force the K.O. with this film, as the KILL TEST does; maps: play the map with 1-7 bots and any MAP EVENTS pace; cosmetics: wear it, see it in films and previews; performances: taunt on the bot's body).
- Import: bring a baked track in from the kill-page
pipeline (
tools/killport), bring sounds in (with loudness and length checks), take shapes from an SVG-like path list, and turn a built-in item into a draft document as a starting point (this is how the proof set starts). - Gate: run the gate locally (a fast subset) or on the VM / M2 (the full gate), and see the report: per-tier frame times, the arena strips, the determinism result, gore / rating, look-check frames.
- Publish: pick the pack, the channel and the rollout, and request approval (a person approves live, D2).
DECIDED (D3, 6 Nov) [user]: the Studio works for all four kinds at launch (option A). Freehand drawing and the posing tool come in a later Studio update, and the data model already allows them (§6.7). The user expects AI agents to build ~95 % of content and people ~5 %, so the headless agent interface is the Studio's primary product, ahead of the visual editor (§6). The visual editor is a view over the same documents and commands.
5.11 The headless CLI (agents)
This is the command surface. §6 describes the full agent-first design around it (schemas, errors, preview, test-play, hot reload, playbook).
tools/studio.sh <command> runs the same Studio
player with -batchmode -scenario studio:<command> (as
cap.sh does for labs). It works on the VM (Linux headless
player, CPU-only steps), on the M2 worker and on the Mac (GPU steps:
frames, video, perf). Every command prints one JSON report and exits
non-zero on failure:
| Command | Does |
|---|---|
new <kind> <id> [--from builtin:<id>] |
a draft document (optionally converted from a built-in item) |
validate <doc> |
schema, ranges, references, budgets (static), rating flags |
compile <doc...> |
the binary docs + the dependency list |
preview <doc> --arena <id> --gore <0,2,5> --tier <low,med,high> [--frames N | --video] |
frames or an mp4 from a real match |
diff <doc> --against builtin:<id> |
the exact-match report for the proof set (§8.1) |
gate <pack> |
the full gate (§5.12); writes the signed gate report |
pack <docs...> |
the .sbp + per-platform bundles |
publish <pack> --channel internal|test|live [--rollout N%] |
uploads to sbapi (needs a publish token, kept in the VM's secret store, never in the repo) |
Agents work in the content repo, which is a git repo of JSON plus
source sounds (DECIDED, D15: a new private repo
farisbasha/stickbash-content; the layout is in §6.4). They
commit by path and run the gate on the VM, and the report is attached to
the publish.
5.12 The quality gate is the publish step
publish refuses a pack without a passing gate report
that has the same pack hash. The gate is today's PORT GATE
(~/StickBrawl-v3/v4/feedback/_tools/PORT_GATE.md) and the
in-game kill performance rules, automated and applied to every kind of
content:
- Static: schema, ranges, references, engine version, bundle contents (§5.9), string table words, static budget (sum of block costs per tier ≤ the kind's budget).
- Determinism: §5.7 rule 6, on every variant.
- Performance per tier: the reference players at LOW
/ MED / HIGH (
FilmQ.LabForce) and with-perfslow 6, on the Mac / M2 (GPU). The headless Linux VM reports CPU ms only, never p95. Frame p95 ≤ 16.7 ms, max ≤ 33 ms, begin ≤ 16.7 ms, 0 B GC per frame in steady state, no hitch at start or end. Per-block budgets are enforced. A block over its budget fails with its name. Before a pack leaves the test channel it also runs the perf and determinism scenario on the reference phones (an iOS phone and a LOW-tier Android, named in the plan). This is a step in the pipeline for every pack, not a one-off check. - Coverage: films on all arenas (home + every arena that fits, plus a clean refusal where it doesn't) at all three gore levels; maps with 2, 4 and 8 fighters, bots only, at every MAP EVENTS pace, HAZARDS on / off, online host + client; cosmetics on every body style, in films and previews; performances on flat, slope and moving floors, and as a gloat.
- Look checks (PORT_GATE §4, the automatic part): limbs through solids, floating or sinking bodies, blood in mid-air or on sky, particles frozen or left after the film, the camera leaving the subject or clipping, black frames, the HUD back after hand-off. A flagged frame goes to the coordinator's review sheet.
- Gore + rating: each level's content against the
ladder and
AgeRules. - Exact match (proof set only, §8.1) and the block tests of every block the item uses (§8.6).
- Human look: the coordinator's side-by-side sheet, then a person's approval for live (D2).
When all of these pass, the gate signs the report and the pack's manifest. The phone checks the signature.
5.13 Server registry, CDN, rollout and rollback
- sbapi
contentmodule (new, Go, besideconfigandseasons):- a
packstable: hash, id, rev, engine,ENGINE_SIMfloor, deps, size per platform, envelope hash, statusdraft → gated → approved → live → retired, who / when; - an item history: item id → its revisions in order (pack hash, status), which is what "the last good version" means;
- a reverse-dependency index: which packs use a pack;
- an upload endpoint that stores the content, bundles and envelope.
- a
- Who can do what (enforced by the server, not by trust;
DECIDED, D2 [user]). Agent tokens can upload and move packs up
to
gated, and publish tointernal/test. Only a human dashboard account can approve and move a pack tolive. The approval is recorded in the envelope. The gate's signing key lives in the VM's secret store, separate from the agents' tokens. A leaked agent token can't reach live players, and a leaked signing key still can't change what is listed live (that needs a human account). - Delivery (DECIDED, D14):
content.stickbash.comis Caddy serving/p/<sha256>.sbp,/e/<sha256>.sbeand/b/<sha256>.<platform>.bundlefrom the VM, with Cloudflare in front (the same Cloudflare + Caddy stack asdeploy/stack/). URLs are content-addressed, so they are cached forever and need no purge. A second origin can be added later without changing the phone.- How things are fronted today (checked read-only on
6 Nov; nothing changed):
stickbash.com,wwwandapi.stickbash.comare on Cloudflare DNS and proxied (server: cloudflare,cf-cache-status: DYNAMIC). The API's origin is the VM's Caddy with a Cloudflare Origin CA certificate, zone SSL mode Full (strict) (deploy/stack/host-site.caddy.tmpl:1-11).relay.stickbash.comis DNS-only to the VM (172.198.61.163), because UDP can't be proxied.bashatest.howincloud.comis direct to the VM's Caddy (not proxied).content.stickbash.comdoes not exist yet.
- To set up before launch (Stage 1, Task C.10; a
person with Cloudflare access does the dashboard steps):
- A DNS record
content.stickbash.com→ the VM, proxied (orange cloud). - A Caddy site block from a new template beside
host-site.caddy.tmpl, with the same Origin CA certificate. It serves only/p/,/e/and/b/from a read-only content directory withCache-Control: public, max-age=31536000, immutable; everything else is a 404. - A Cloudflare cache rule for that host: cache
everything, edge TTL ≥ 1 month, respect origin headers. Without it
Cloudflare would not cache
.sbp/.bundlefiles by extension. - An upload path from sbapi's
contentmodule into the content directory, written once per hash and never overwritten. - A check that a cached file is served from the edge
(
cf-cache-status: HIT).
- A DNS record
- Files stay ≤ 5 MB (D6), which is within normal CDN use of the Cloudflare plan.
- How things are fronted today (checked read-only on
6 Nov; nothing changed):
- What is live: its own versioned document. Content
gets its own document, the content list: per channel,
the enabled pack hashes, the rollout percentage (a stable bucket of the
player id), platform and minimum app version. It uses the same publish /
version / rollback machinery as the live config
(
publish.go:212-214: rollback re-publishes an old version as a new, higher one), but it is a separate document. A content rollback never reverts prices or map looks, and a config rollback never brings content back. - The kill list only grows. It is a separate short
list of pulled pack hashes, served with the content list and never
rolled back. Un-pulling a pack is an explicit new entry ("restore
"), done by a human. A pulled pack stops being offered and stops playing at the next fetch. Pulling cascades: every pack that depends on it (the reverse index) is pulled with it, and the dashboard shows that list before the human confirms. - Owned items when a pack is pulled. The phone keeps
each owned item's current pack and its previous live revision pinned
(§5.1).
- If the item has an earlier live revision, it falls back to that one.
- If it has none, the item hides but stays owned, and comes back when a fixed revision goes live.
- A premium skin whose attached film is pulled keeps its look and plays the built-in film for that weapon until the fix.
- DECIDED (D13): they keep it, as above.
- The phone: download at app open, blocking with progress
(DECIDED, D4 + D5, 6 Nov) [user + coordinator's call]. The
user: "initially most of the time we block app until download complete,
like we show downloading content progress in splash or something, u
decide".
- At app open the splash fetches the content list and
downloads every required or new pack for this app (its channel, platform
and
ENGINE_API), on any network, before the menu. It shows a progress bar: "Downloading new content… 3.2 / 7 MB". The per-pack and total size caps apply (D6). - Every pack is verified (hash + signature) and stored, and only then registered in the catalogue. A pack that is half downloaded or fails to verify never shows.
- Offline or failing: after a short retry (about 3 tries over ~10 s), the app continues with its cached packs and shows a small "new content waiting" note, retrying in the background. It never blocks forever.
- Mid-session: a pack published while the app is open is fetched in the background when the next content list arrives.
- Rooms still verify at join (§5.8). If a playable pack is missing (for example one published mid-session), the joiner downloads it with a progress bar and then joins. The room can't start until every member has it.
- Downloads and failures go to the existing telemetry path.
- At app open the splash fetches the content list and
downloads every required or new pack for this app (its channel, platform
and
- Channels:
internal(our devices, the Studio),test(opted-in testers and the VM bots),live. The rollout order is DECIDED (D9): our test phones → 10 % of players for a day → everyone, with a one-tap pull-back at every step.
5.14 The shared catalogue
- One interface,
ContentCatalog, lists every item: built-in items (fromCosmeticsCatalog,PageKill,Arenas,Taunts) and engine items (from registered packs), each as oneCatalogItemwith id, kind, slot, rarity, source, price, theme tags,sinceVersion/engine, owned state, preview hook and the content to play. The shop, pass, rooms, customize, kill shop, album and preview screens read only this interface. Built-ins are adapted, not changed. - Ownership is by id. An engine item's id never changes across revisions, so a fix never takes an item away.
- Server side:
CatalogExportadds pack items (withengineinstead of onlysinceVersion). sbapi's season auto-pick treats an engine item as pickable for an app version only when that version'sENGINE_API≥ the item'sengine, and only when its pack is live on that platform. Prices and pass rewards stay in the live config, as today. - The migration switch (later, [user]): a
migratetable in the content list maps a built-in key to an engine document ("revolver.six-shooter": "eng.revolver.six-shooter"). The catalogue then plays the engine version under the built-in id. Rollback = remove the row (a content-list rollback). Before launch this switch exists but is empty, and only internal builds can set it for the proof set.
5.15 Store compliance (why this is allowed)
- Everything downloaded is data (documents of block ids and numbers, strings, audio, images) read by code that shipped in the reviewed app. There is no interpreter for a language. The logic graph (§5.5) is a fixed-size network of app-shipped parts: no instructions, no named variables, no loops except through one-tick state nodes, no functions, and a hard cap on nodes and state. The tiny formula set (§5.5a) only computes one number for a declared slot from fixed inputs with fixed arithmetic operations (≤ 32 nodes, no loops, variables, strings or functions). It has a server kill switch that falls back to constants.
- Bundles hold no scripts or shaders, and the gate and the phone both check this.
- New features (new blocks) arrive only in app updates that go through review.
- Pushed content stays within the app's age rating and the no-advantage rule.
- The App Store review notes and the Play listing will say: "The app downloads new cosmetic items, arenas and animations as data files; no executable code is downloaded" [call].
6. AGENT-FIRST AUTHORING
DECIDED (D3, 6 Nov) [user]: "most of the time the ai agent will build this, I mean 95% of the time, 5% of time we may manually create maps, so prioritize things make ai agent easier and flexible and customizable".
In plain words. The main user of the Studio is an AI helper typing commands, not a person dragging things. So the Studio comes with:
- a complete written rulebook for every kind of item;
- error messages that say exactly what is wrong and how to fix it;
- pictures and videos the helper can look at to check its own work;
- test matches against bots that report numbers;
- ready-made templates and worked examples;
- a step-by-step playbook for making a map, a film, a skin or a taunt.
A change should show up as a picture in under 30 seconds. The visual editor, and later the drawing and posing tools, sit on top of the same files and commands, so anything a helper makes a person can open, and the other way round.
6.1 The schema: one versioned, documented contract per kind
- A JSON Schema (draft 2020-12) per document kind:
film,map,cosmetic,performance, plus the sharedrig,palette,soundset,pathandlogic. Every block has its own sub-schema. The schema is versioned withENGINE_APIand generated from the app's block classes (the same parameter declarations the runtime validates against), so the schema, the runtime and the docs can never disagree.studio.sh schema --kind mapprints it, and it is also committed to the content repo at every engine bump. - Generated block reference
(
docs/blocks/<block>.mdin the content repo, one page per block). Each page holds:- what the block does and when to use it;
- a parameter table with name, type, unit, grid, range, default and meaning;
- its tier and gore rules, its determinism class (
sim/look), its cost formula and budget, and its attach points; - the logic-graph sources and sinks it exposes;
- one minimal example and one realistic example, both validated and rendered at every engine bump (the page embeds the preview still);
- "see also" links and common mistakes.
- Stable names agents can rely on: block ids, parameter names and enum values never change meaning (append-only, §5.8). A deprecated parameter keeps working and lint warns about it.
- Where a formula may replace a constant. Every
parameter row in the schema and on its reference page has an
exprcolumn (none/look/sim, §5.5a) and lists the inputs allowed for that slot. In JSON, a slot holds either a constant ("glow": 0.6) or a formula object with its mandatory fallback ("glow": { "expr": "clamp(lerp(0.2, 1, speed / 6m/s), 0, 1)", "else": 0.6 }).validatereports formula errors with the path, the character position and a fix hint ("inputenemyDistanceisn't allowed on a cosmetic; usespeed,health,airborne,time").lintwarns when a formula is constant (fold it), or when its fallback looks very different from its typical value.preview --formulas offrenders the fallback look. - Units everywhere are explicit. Authors write
metres, seconds and degrees with a unit suffix
(
"len": "3.85m","at": "1.2s","at": "72t"), and the compiler quantises them. Bare numbers are refused where a unit matters, so an agent can't confuse page units, metres and ticks.
6.2
The command interface: studio.sh and the same commands over
JSON-RPC
Every command takes files or JSON on stdin and returns one
machine-readable JSON result (plus a short human summary on
stderr). Exit codes: 0 ok, 1 errors, 2 warnings only, 3 tool failure.
The same commands are served over a local JSON-RPC socket by the running
Studio (studio.sh serve, §6.5), so an agent can call them
without starting a process each time.
| Command | What it gives the agent |
|---|---|
schema --kind <k> [--block <b>] |
the schema / one block's reference as JSON |
new <kind> <id> --template <t> /
--from builtin:<id> |
a draft from a template (§6.3) or converted from a built-in item |
validate <doc> |
precise errors, each with a JSON path
(/blocks/3/p/period), the rule broken, the value found, the
allowed range and a fix hint ("period must be 0.5s-60s;
you wrote 0.2s: raise it or use map.rotor for fast spins").
It also explains reference errors ("rig.ink-sword not
found; did you mean eng.core-props/rig.ink_sword?") |
lint <doc> |
style and budget warnings: over 80 % of a tier budget, a hazard warning under 0.8 s, unused nodes, a logic graph that can never fire a sink, a film prop that never hides, palette colours outside the game's look range, a text string that fails the word check, a map spawn too near a hazard |
explain <doc> |
a plain summary of what the document does over time (timeline + logic, in words). Agents use it to check that their intent matches what they wrote |
preview <doc> [--arena] [--gore] [--tier] [--seed] [--at 1.2s,2.5s | --every 0.25s | --video] [--cam fit|follow:<id>] |
stills (PNG) or an mp4 from the real game, with a frame-index sheet and a contact sheet, for the agent's own visual check |
play <map|doc> --bots N --mode <m> --rounds R --seed S [--pace calm|normal|chaos] |
test-play vs bots. A metrics report: deaths by cause (hazard / fighter / ring-out), time near each hazard, spawn fairness, bot stuck time and unreachable spans, path coverage heat map, round length, event firings, film fits / refusals per arena, perf (p50 / p95 / max) |
gate <pack> [--fast] |
the full gate (§5.12), or a fast local subset |
diff <doc> --against <rev|builtin:id|file> |
what changed vs a previous revision: parameter diffs, cost diff, a visual diff strip at the same seed and ticks, logic-graph diff, and the exact-match report for the proof set |
pack /
publish --channel internal|test |
build the pack and send it to our own channels. live
needs a human approval (D2) |
docs <block> |
the block's reference page |
6.3 Templates and worked examples
Templates per kind:
- Maps: one-screen arena, MEDIUM follow map, BIG closed map, hazard arena, logic-puzzle arena.
- Films: round-end film, generic gun-class film, skin film, mythic.
- Cosmetics: hat, face, outfit, aura, trail, plain weapon skin, premium skin (film + ammo + sparks + burst + feed icon).
- Performances: emote, taunt, gloat, win dance.
Each template is a valid document with comments in its
studio.notes, and it passes the gate as is.The worked-example library is the proof set as reference documents. Each proof doc is annotated block by block ("this is DRIP's slab; this is how a bullet hole state is wired"), and its exact match proves it is correct. That makes them the best possible examples. More examples come with every new block (each block page links one).
Recipes: short, validated snippets for common jobs: a door with two plates, a timed hazard cycle with a warning, lava that rises when two are left, a prop that is cast from ink and leaves a mark, a skin that pulses on a kill, a gloat that sits on the body.
6.4 The content repo layout (for agents)
stickbash-content/
schema/<ENGINE_API>/*.json generated, committed at each engine bump
docs/blocks/*.md docs/playbook/*.md
templates/<kind>/*.json examples/proof/*.json recipes/*.json
packs/<pack-id>/pack.json the pack's manifest source (deps, channel notes)
packs/<pack-id>/<doc>.json one document per file
packs/<pack-id>/sounds/*.wav source sounds (imported by the bundle lane)
packs/<pack-id>/takes/*.bytes imported film takes
out/ previews, reports (git-ignored)
One document per file, stable key order, and
studio.sh fmt to canonicalise, so diffs stay small and two
agents rarely conflict.
6.5 Fast iteration: under 30 s from edit to preview still
- No Unity build for content changes. Content is data, so the same Studio player loads any document at runtime. A Unity build is only needed when a block changes (an app change).
studio.sh servekeeps one Studio player running headless (or windowed on the Mac) with the arenas, fighters and the shape pipeline warm. It watches the content repo and hot-reloads a changed document in place, re-validating and re-compiling only that document and the ones that depend on it.- Targets on the Mac / M2:
- validate + compile < 1 s;
- hot reload < 2 s;
- a preview still at a given tick < 10 s;
- a 6-still contact sheet < 20 s;
- edit → first still < 30 s;
- a 10-second mp4 < 60 s;
playwith 3 bots for one round < 90 s (fixed-step fast-forward where perf isn't measured).
- On the VM, still and video runs go to the M2 / Mac lanes by the existing load rules, and validate / lint / logic checks run locally.
- Deterministic seeds everywhere: every
preview,play,gateanddifftakes--seedand prints the seeds it used. The same seed, document and engine build give the same frames and metrics, so an agent can compare before and after a change and trust that the difference is its own edit.
6.6 The agent playbook
docs/playbook/ in the content repo holds one page per
kind (map, film, skin, taunt), each a numbered checklist the agent
follows:
- Read the brief.
- Pick a template or a proof example.
- List the blocks to use.
- Write the document.
validateuntil clean, thenlintuntil clean or each warning is justified.explainand compare the summary with the brief.previewthe key moments.play(maps: 2 / 4 / 8 bots, every pace) and read the metrics against the targets on the page.gate --fast.diffagainst the last revision.- Write the check sheet (stills, metrics, gate).
publish --channel testand request approval.
Each page also lists the kind's look rules, taken from the existing taste notes (e.g. USER_TASTE.md and the cinematic and realistic action direction for films), the fairness rules for maps (warnings, spawn distance, no unwinnable spots), and the no-advantage rules for cosmetics. These are rules an agent can check, not taste it has to guess.
6.7 Ready for freehand drawing and posing later (no data-model change)
- Drawing produces the data that already exists:
shapeprimitives andpaths (points on the mm grid) insiderigs. A freehand stroke is saved as a simplifiedpathplus stroke width and fill. There is no new block and no new document field. - Posing produces
perf.posekeys (taunts) or afilm.take(films), the same 13-joint, 60 Hz format the kill pages bake today. A posed take and an imported take are the same data. - Editor-only data (layer names, guides, onion-skin settings) lives in
the document's
studioblock, which the compiler strips. Adding a tool never changes a pack.
7. NEW BLOCKS BEYOND TODAY'S GAME
In plain words. A pushed item can only use bricks that are already inside the app. If a brick is missing on launch day, no item can use it until the next app update. So the launch app carries today's bricks (§5.2) plus many new ones the game has never had: moving and spinning platforms, switches and doors, portals, lasers and saws, spawners, rising water, weather, new camera moves, new skin materials, skins that react when you hit or win, and taunt props. The logic graph (§5.5) lets us wire them together into behaviours nobody has built yet, such as a door that opens when two players stand on two plates, or lava that rises faster when only two fighters are left [user, 6 Nov]. DECIDED (D1, 6 Nov) [user]: the launch app carries today's blocks plus the ~61 T1 blocks below (option A).
7.1 Rules every new block follows
- Ranked by value. "Value" means how many new items the block unlocks, times how much players notice it, divided by build and risk cost. T1 = the recommended launch library. T2 = the first app update after launch. T3 = later, or only if a design needs it.
- Online. Each block is either
sim(it changes the match: host-authoritative, state in the snapshot through the generic writer, its pose a pure function of(state ints, tick)) orlook(drawing and sound only, run per phone, never hashed) (§5.7). Asimblock that pushes or carries fighters must be predictable: its force or motion is a pure function of(position, state, tick)that the client can run for its own fighter, asMapEvent.Accelalready is for gusts (MapEvents.cs:185). Asimblock also declares its bot rule (avoid, wait for a phase, ride, use) soNavGraphand the bots handle it. - Fairness. Every harmful
simblock has a warning of at least 0.6 s (the gate checks it), follows the room's HAZARDS switch, and is spared during kill films and executions, as today's hazards are. Cosmeticlookblocks keep the no-advantage rule: hard caps on size, alpha and screen area, never on or near the hitbox, and no information a player wouldn't otherwise have. - Budgets. The numbers below are starting
caps on the LOW reference phone: host CPU per fixed step for
sim, and shapes per frame for drawing. Stage 1 (and each engine's stage) calibrates them so that a map's or film's engine blocks together never cost more than today's heaviest code map (Pinball / Wrecking Yard) or heaviest film at each tier. After that the gate enforces them (§5.12), and the phone's budget meter dropslookextras first.
7.2 Maps: new elements and logic
| # | Tier | Block | What it does | Parameters | Online | Budget (LOW) |
|---|---|---|---|---|---|---|
| 1 | T1 | logic.* (the logic graph, §5.5) |
Wires every block below into new behaviour | nodes, wires, constants | sim on the host; packed state ≤ 64 B in the
snapshot |
≤ 256 nodes, ≤ 60 µs / step |
| 2 | T1 | map.zone |
An area that senses who is in it: enter, leave, count, team, held time | shape (box / circle / poly), filter (fighters, bots, props, items, team), min hold ticks | sim, host overlap test; outputs bools / counts |
≤ 32 zones, ≤ 3 µs each |
| 3 | T1 | map.mover |
A platform, prop or hazard that moves along a spline | path (≤ 32 points, open / closed, Catmull-Rom or line), period or speed, ease per segment, dwell per node, ping-pong, start phase, carries riders | sim, pose = f(tick, phase, paused-by-logic);
predictable |
≤ 24 movers, ≤ 4 µs each, 1 collider |
| 4 | T1 | map.rotor |
A rotating or orbiting platform (wheel, windmill, clock hand, satellite planks) | pivot, arms 1-8, radius, angular speed curve, start angle, reverse on signal, carries riders | sim, f(tick); predictable (slopes underfoot as
MapPlat.rot today) |
≤ 8 rotors × 8 arms, ≤ 6 µs each |
| 5 | T1 | map.switch |
A thing you hit, stand on or touch to flip a signal | kind (lever, button, target), momentary / toggle / one-shot, cooldown, who may use it, look rig | sim, host; state bit in the snapshot |
≤ 32, ≤ 2 µs |
| 6 | T1 | map.door |
A sliding, swinging or folding solid that opens / closes on a signal | piece size, motion (slide vector / hinge angle), open / close ticks, ease, crush-safe (stops on a body), auto-close delay | sim, f(open ticks); bot rule: wait while shut |
≤ 16, ≤ 3 µs |
| 7 | T1 | map.laser |
A beam: fixed, sweeping or blinking | ends or pivot + sweep curve, on / off pattern, warning blink ticks, effect (damage / K.O. / push), blocked by solids (yes / no) | sim, f(tick, state); raycast on host only |
≤ 6, ≤ 15 µs with blocking |
| 8 | T1 | map.saw |
A spinning blade on a path or a fixed spot | radius, path (via map.mover), spin, damage per touch,
sparks, gore cut rule (follows the gore level) |
sim, f(tick); gore is look |
≤ 8, ≤ 4 µs |
| 9 | T1 | map.crusher |
A piston or ceiling block that slams down and rises | travel, down / hold / up ticks, warning shake, effect (K.O. if pinned against a solid), safe stop on props | sim, f(tick, cycle); bot rule: avoid during
warning |
≤ 8, ≤ 5 µs |
| 10 | T1 | map.spikes |
Spikes that pop out of a floor or wall on a timer or signal | span, out / in ticks, warning ticks, damage or K.O. | sim, state bit + phase tick |
≤ 16 spans, ≤ 2 µs |
| 11 | T1 | map.dropper |
Things falling from above (rocks, anvils, crates, icicles) with a shadow warning | drop points or an x range (seeded pick), object kind (built-in props / rigs), interval curve, warning ticks, effect | sim, the host picks with the seeded stream; picks ride
the snapshot |
≤ 4 in the air, ≤ 6 µs |
| 12 | T1 | map.spawner |
Spawns weapons, items or crates at points on a rule | pool (built-in weapon / item / prop ids with weights), points, interval, max alive, first-spawn tick, seeded pick | sim; props are pre-built and parked (the
MapReserve rule, MapNewKit.cs:14-15) so the
props block stays in build order |
pool ≤ 16 parked, ≤ 3 µs |
| 13 | T1 | map.level |
Water or lava whose level follows a timeline or the logic graph | kind (water / lava / acid look of lava), level curve over fight time, speed, warning, max | sim, level = f(tick, state); existing flood / lava
rules |
1 per map, ≤ 5 µs |
| 14 | T1 | map.field |
Gravity, low-g, wind or current zones with a strength that changes | area, kind (gravity scale / push vector / swirl), strength curve or signal-driven, gust pattern (seeded) | sim, predictable Accel(p) |
≤ 8 fields, ≤ 2 µs per fighter |
| 15 | T1 | map.phase |
Platforms that fade in and out (blink, alternate sets, appear on a signal) | pieces, on / off ticks or signal, warning blink, one-way or solid | sim, state bit |
≤ 32 pieces, ≤ 1 µs |
| 16 | T1 | map.tiles |
A grid of tiles that break from hits or standing and grow back | grid, tile size / material, break rule (hits / stand ticks / explosion), shake ticks, regrow ticks, never-break set | sim, bitmask in the snapshot (generalises Crumble
Tiles) |
≤ 64 tiles, ≤ 4 µs |
| 17 | T1 | map.belt |
Conveyors whose direction and speed change | pieces, speed curve, reverse on signal, warning | sim, predictable (today's PlatDef.conveyor
made dynamic) |
≤ 8, ≤ 1 µs |
| 18 | T1 | map.bumper |
Bumpers, trampolines, bounce walls | shape, bounce factor, fixed launch vector or normal, cooldown | sim, predictable |
≤ 16, ≤ 1 µs |
| 19 | T1 | map.plate |
A pressure plate that counts weight (fighters + props) | area, threshold, hold ticks | sim, a preset of map.zone |
as map.zone |
| 20 | T1 | map.portal |
Teleporter pairs | in / out shapes, keep / rotate velocity, cooldown per body, what passes (fighters, props; projectiles in T2), on / off signal | sim, host; predictable for the own fighter (pure
f(position, state)), with the host confirming |
≤ 4 pairs, ≤ 3 µs |
| 21 | T1 | map.weather |
A day / night and weather timeline: rain, snow, fog, lightning flashes, gust visuals | keys over fight time; the per-map night lock 85-100
(MapLookConf) still applies |
look; any push it causes is a
map.field |
≤ 400 shapes |
| 22 | T1 | map.camrule |
Camera rules per area or phase: zoom limits, axis lock, look-ahead, a framed "arena" moment | areas, limits, ease ticks | look (local camera) |
≤ 8, negligible |
| 23 | T1 | map.ambience |
Positional looping sounds and one-shots tied to signals | clip, position / area, volume curve, signal | look |
≤ 6 voices |
| 24 | T2 | map.objective |
Score zones / king-of-the-hill / capture points | zone, points per tick, contest rule, moving zone via timeline | sim; needs a generic objective rule in the mode system
(app code). Ship the hook in T1 if KOTH maps are wanted early |
≤ 4, ≤ 3 µs |
| 25 | T2 | map.light |
Spot cones, flicker, lights you can shoot out | cone, colour, flicker pattern (seeded), breakable | look; a shot-out light is a sim bit |
≤ 6 |
| 26 | T2 | map.creature |
A creature rig (shark, kraken arm, drone) on a mover with a bite / hit zone | rig, path, attack zone + warning, cooldown | sim via mover + hazard |
≤ 2 |
| 27 | T3 | map.rope |
Ropes, ziplines, swings | anchors, ride speed | needs new fighter movement (gameplay code) | n/a |
Logic examples the T1 set makes possible, with no new code: a door that opens only while two plates are held (zone ×2 → AND → door); a "last two standing" finale where lava rises and lasers start (alive count ≤ 2 → latch → level + laser); a switch that reverses every belt and spins the rotors the other way (switch → toggle → belt + rotor); a vault that opens after a sequence of three targets is hit in order (switch ×3 → sequence → door → spawner); a seeded random event every 30 s (timer → select(random) → dropper / spikes / field gust).
7.3 Films: new camera, effects, time, sound
All film blocks are look. A film's sim part
is only its end pose and the victim's death state, and the gate checks
that no new block changes them (§5.7).
| # | Tier | Block | What it does | Parameters | Budget (LOW) |
|---|---|---|---|---|---|
| 1 | T1 | cam.path |
Dolly / crane moves along a spline, with framing that eases toward a target | path, speed curve, target (joint / piece / point), roll curve, zoom curve | negligible |
| 2 | T1 | cam.ride |
The camera rides a round, a thrown weapon or a severed piece (bullet cam for any projectile) | subject, offset, lead, roll-with-motion, exit ease | negligible |
| 3 | T1 | cam.handheld |
Seeded handheld shake profiles (breathing, run, impact) | profile, amplitude curve, seed stream | negligible |
| 4 | T1 | cam.snap |
Crash zoom, snap pan, smash cut with a flash frame | target, ticks, overshoot, flash colour | negligible |
| 5 | T1 | cam.freeze |
Frozen moment with a camera move through it (bullet time in 2D: roll, zoom and slide while the clock is 0) | window, camera keys, release ramp | ≤ 1 extra full redraw |
| 6 | T1 | fx.impact |
1-3 frame impact frames: two-tone, invert, silhouette, colour-key | ticks, style, colours | 1 full-screen pass |
| 7 | T1 | fx.speed |
Speed lines and radial streaks (the existing
RadialBlurFeature on HIGH; shape lines on LOW) |
centre, density, length, life | ≤ 120 shapes / blur on HIGH only |
| 8 | T1 | fx.ring |
Shockwave rings, dust rings, ground cracks spreading out | centre, radius curve, thickness, colour, count | ≤ 40 shapes |
| 9 | T1 | fx.ghosts |
Afterimages of a body or weapon at intervals | subject, count ≤ 6, gap ticks, alpha fall-off, tint | ≤ 6 body redraws |
| 10 | T1 | fx.transition |
Ink splash wipe, iris, comic-panel slide, shatter | style, ticks, origin | 1 overlay |
| 11 | T1 | fx.screen |
Screen-space overlays: crack, frost creep, blood-free ink splatter, vignette pulse | style, ticks, strength (gore-gated where red) | 1 overlay |
| 12 | T1 | time.stutter |
Frame-hold patterns (hold 2, play 1) for a beat | window, pattern | negligible |
| 13 | T1 | snd.beat |
Snaps a hit or a stinger onto the music kit's beat / bar | cue, kit, grid (beat / bar / half), max shift ticks | negligible |
| 14 | T1 | snd.layers |
Switches music-kit layers (bed → pulse → drums → riser → hit) on cues | kit, layer per cue, crossfade ticks | ≤ 4 voices |
| 15 | T1 | snd.silence |
A full mute window, then the slam back | window, what stays (heartbeat / nothing), slam gain | negligible |
| 16 | T1 | snd.slowmo |
The slow-motion sound morph as a curve (pitch, low-pass, reverb send) tied to the clock | curve shape, floor Hz | negligible |
| 17 | T2 | time.rewind |
A rewind-and-replay window (the moment again from another camera) | window, rewind speed, replay camera | ≤ 1 extra decode |
| 18 | T2 | cam.autocut |
Auto multicam: cut between named shots on cues and beats | shot list, cut rules | negligible |
| 19 | T2 | fx.xray |
An x-ray window on impact (the old KillCineHud x-ray;
anatomy follows the gore level) |
window, joint, size | 1 inset |
| 20 | T2 | fx.lens |
Chromatic split, lens dirt, heat haze | strength, ticks | 1 pass, HIGH only |
| 21 | T2 | snd.heartbeat |
A heartbeat bed whose rate follows the clock speed | clip, rate curve | 1 voice |
7.4 Skins and cosmetics: materials, patterns, reactions
All are look, under the no-advantage caps (§7.1).
Materials and patterns need one app-shipped feature in
Shapes.shader (layers, gradients, pattern
functions). That is a shader change in the app, precompiled at build,
and packs only set its numbers [user constraint: no shaders in
packs].
| # | Tier | Block | What it does | Parameters | Budget (LOW) |
|---|---|---|---|---|---|
| 1 | T1 | mat.layers |
Up to 4 material layers on any body part, weapon part or rig: base, gradient, metallic sheen, rim, outline, emissive | per layer: kind, colours, angle, strength, blend | +0 shapes, shader ALU only; LOW drops to 2 layers |
| 2 | T1 | mat.pattern |
Procedural patterns: stripes, dots, checker, hex, camo noise, cracks, circuit, flames, scales | kind, scale, angle, colours (2-3), seed | shader ALU; LOW keeps it |
| 3 | T1 | mat.animate |
Animates a material: scroll, pulse, hue cycle, shimmer sweep | target param, curve, speed | negligible; LOW halves the rate |
| 4 | T1 | cos.anchors+ |
New attach points: shoulders, hips, feet, weapon muzzle / stock / magazine / grip, orbit points around the head, the ground under the feet | (fixed list, app-shipped) | negligible |
| 5 | T1 | cos.react |
Reaction hooks: on fire, reload, hit dealt, hit taken, K.O., death, idle N s, jump, land, pickup, win, low health | event, effect (any look block: fx, pattern pulse,
ribbon burst, sound layer), cooldown, cap |
≤ 1 reaction live per fighter on LOW |
| 6 | T1 | cos.emitter |
Particles attached to an anchor: embers, sparks, snow, petals, smoke, glitter | preset, rate cap, life, colour, spread | ≤ 24 live per fighter (LOW 8) |
| 7 | T1 | cos.ribbon |
Ribbon trails from a joint or weapon part | anchor, width curve, life, colours, pattern | ≤ 2 ribbons × 32 segments |
| 8 | T1 | cos.aura |
Aura layers: rings, glow shells, floating orbiters | layers ≤ 3, radius, pulse, orbit count | ≤ 40 shapes |
| 9 | T2 | cos.dangle |
Look-only physics bits: tails, capes, chains, ear flops (small verlet) | anchor, links ≤ 8, stiffness, gravity | ≤ 2 chains, ≤ 15 µs |
| 10 | T2 | mat.flipbook |
A texture-atlas flipbook on a part (needs a raster bundle) | atlas, fps, loop | 1 texture ≤ 512² |
| 11 | T2 | cos.sound |
An equip sound and a quiet layer on the weapon's fire (never masks gameplay sounds; loudness cap) | clips, gain cap | 1 voice |
7.5 Taunts and emotes: layers, props, FX
The taunt start stays host-decided: Ev.Taunt carries a
ContentRef and poses arrive in the snapshots
(Taunts.cs:13-15). Props and FX are look.
| # | Tier | Block | What it does | Parameters | Budget (LOW) |
|---|---|---|---|---|---|
| 1 | T1 | perf.blend |
Spring / overshoot blending between keys, mirror, speed scale | spring, damping, mirror, speed | negligible |
| 2 | T1 | perf.layer |
Pose layers: a face / arm / head layer over a base taunt (one base, many variants) | base ref, layer keys, weight curve | negligible |
| 3 | T1 | perf.toss |
Props that are thrown, dropped or kicked, land and stay for a while (look-only physics) | rig, launch, bounce, rest ticks | ≤ 3 props, ≤ 10 µs |
| 4 | T1 | perf.fx |
Confetti, sparkle, emoji bubbles, smoke puff, ink pop, coins | preset, count cap, anchor | ≤ 40 particles |
| 5 | T1 | perf.aim |
Aims a pose or prop at the KO'd body or the nearest opponent (point at, step over, look down) | target, joints, limits | negligible |
| 6 | T1 | perf.sting |
A short music sting with a duck | clip, duck level, ticks | 1 voice |
| 7 | T2 | perf.walk |
An upper-body taunt layer while walking (a rules change: today's taunts need you grounded and free) | layer, allowed moves | negligible |
| 8 | T3 | perf.duo |
Two-player synced taunts | partner rule, sync | needs a wire change |
7.6 Shared blocks
| # | Tier | Block | What it does | Parameters | Online / budget |
|---|---|---|---|---|---|
| 1 | T1 | curve |
Every animatable number: Bézier, the TE eases, step,
spring, bounce, seeded noise |
keys, ease, seed | integer keys; negligible |
| 2 | T1 | rng.stream |
Deterministic random. Seed = hash(round or film seed, content id, instance, stream id). Draw counts are checked by the gate | stream id | sim streams only on the host; look streams
per phone; never the global Rng |
| 3 | T1 | cond |
Conditions: compare a source (fight time, alive count, score, phase, team, gore, tier, platform) with a constant | source, op, constant | i16 compare; negligible |
| 4 | T1 | path |
A shared spline used by movers, cameras, props, ribbons | points ≤ 32, kind | integer points |
| 5 | T1 | strings |
A localised string table per pack (names, signs, words), word-checked by the gate | lang → text | look |
| 6 | T1 | palette.set |
Palette sets, e.g. one per season theme, swapped by the season | slots, theme id | look |
| 7 | T1 | variance |
Per-instance jitter within declared ranges (seeded): no two drops, sparks or spawns identical | param, range, stream | as rng.stream |
| 8 | T1 | shape.lib |
Ready-made shape parts: chain link, flame, spike, gear, star, crack, drip, bolt | part, size, colours | as shape |
7.7 Counts and cost
| Family | Today's blocks (§5.2) | New T1 | New T2 | New T3 |
|---|---|---|---|---|
| Maps | 13 | 23 | 3 | 1 |
| Films | 13 (+ common) | 16 | 5 | 0 |
| Skins / cosmetics | 4 | 8 | 3 | 0 |
| Taunts / emotes | 7 | 6 | 1 | 1 |
| Shared / common | 13 | 8 | 0 | 0 |
| Total | ~50 | ~61 | ~12 | ~2 |
The T1 set is about 35-45 lane-days of the plan, spread over the engines that own each family (§10). T2 adds about 15-20 more.
8. The proof set
The proof set is built in the engine only to prove it can match the originals exactly. Players keep the originals [user]. Each pick covers different blocks, so together they exercise every family in §5.2 before launch.
8.1 What "exact match" means (DECIDED, D7: identical)
The rule (D7: zero difference per frame in shapes, sounds and positions): for every proof item, for every gore level and tier and for a fixed seed, the engine version and the original are run on the same machine at the same gore level. They must produce
- the same full state on every tick: maps: pieces, hazards, props, event phases, director draws; films: both bodies' joints, the end pose, cuts and pieces. Cuts are allowed here because both sides use the same gore level. This is a local comparison, not the online hash;
- the same draw list (the shapes sent to
ShapeBatch, compared after the same quantisation) on every sampled frame, with a pixel fallback (SSIM ≥ 0.99 on the whole frame) only where an ordering difference doesn't change the image; - the same sound events (voice id, tick, vol, rate) and the same Rng draw counts.
Online, each engine proof film and map also passes a host + client
run (Mac host, phone client) with matching online hashes (§5.7).
studio.sh diff prints all of this per item. It is stricter
than the PORT GATE's page parity (2 page units, region SSIM ≥ 0.50),
because here both sides are the same game.
8.2 Three maps
| Map | Why it is in the set | Blocks it proves |
|---|---|---|
Backyard (Arenas.cs:214-216) |
The simplest classic arena and the home arena of most films. It proves the base: exact geometry, one-way planks, spawns + extra spawns, theme, and the film fit checks run on an engine map. | map.frame (open, fit-all), map.solid,
map.spawns, map.art, map.look,
map.elements |
Belt Line
(Scripts/Level/MapBeltLine.cs) |
Moving surfaces and a scripted timeline. Two conveyors carry everyone to a central grinder, the belts reverse on a cue, a crate shower falls from reserved props, the timber plank breaks, ladders lead up to steel decks, and the small closed-arena frame is used. | map.solid (conveyor), map.hazard (kill
zone: grinder), map.timeline, map.event
(warning + phases), map.prop (reserved),
map.piece (breakable), map.traverse
(ladders) |
Wrecking Yard (MapWreck.cs) |
The newest MEDIUM map and the heaviest sim map: follow
camera, 2-8 fighters, three pendulum spike balls posed from the swing
clock, solid balls, a breakable platform and a 16-brick wall with
rebuild, the SMASH event (chain let down), the PALLET event, and an
event payload on the wire today written by hand. |
map.frame (closed, follow), map.hazard
(pendulum, solid, HAZARDS off), map.piece (breakable
bricks, rebuild), map.event (generic payload vs
MapWreck.cs:341-352), state machines, bot nav over
pieces |
Alternate, if the user wants weight-driven physics in the set: swap Belt Line for Seesaw Pier (a plank tipped by the weight on it, spring + damping + caps, the sea as ring-out).
8.3 Six films
| Film | Why it is in the set | Blocks it proves |
|---|---|---|
SIX-SHOOTER revolver.six-shooter
(PageKills/SixShooter.cs, 22 lines, no hooks) |
The pure-data baseline: if this doesn't match, nothing will. Layered multicam on three rounds, six limb severs at EXTREME, head pop, the gore ladder LOW / MEDIUM / EXTREME from the take. | film.take, film.cast, camera
(insets / panels), film.cut, film.blood,
film.soul, sound |
THE X smg.the-x
(Scripts/Core/TheX.cs:27-34, wire id 1) |
The first and reference film. A signature glyph, the single-camera
switch, a hat knocked off, and the wall X that stays after the film
(drawAfterUnder). |
text (glyph), camera (single cam),
film.prop (hat, outlives the film), film.mark
(wall X), film.order (killer over), clock,
driver |
DRIP revolver.drip
(PageKills/Drip.cs:9-23) |
The heaviest prop film. The real game knife moves between hands and flies with a severed hand along the page arc, an ink-cast gold slab gets a bullet hole, a drip line is left on the floor, brass falls, the track is patched at MEDIUM (no head pop), and voices are dropped. | film.weaponprop, film.prop with states,
film.cut (piece path), film.mark,
film.trackedit, fx, sound edits,
counter |
HIGH NOON pistol.gen-high-noon
(PageKills/GenHighNoon.cs:10-14,33-40) |
The generic film: one design for every gun of a class (a track per class on a template gun, the real gun drawn and fired on it). This is the "more content, faster" multiplier. | film.cast (gun-class binding, trackOf),
film.weaponprop (killer's real gun on the template row),
film.fit |
COLD-BLOODED revolver ICE skin film
(PageKills/ColdBlooded.cs:5-21,34,41) |
A skin's attached film, different at each gore level: frost spreading over the body, a slanted shaft of sun with the killer's projected shadow, a sealed (ice) forearm cut kept on the corpse at EXTREME, a forked tongue and breath fog, dropped words. | film.cast (skin slot), look (frost spread,
warm / shade tint), grade (light shaft + projected shadow),
film.cut (sealed, cap colour), gore variants,
rig |
THE OFFERED HAND (mythic,
PageKills/TheOfferedHand.cs) |
The weapon-agnostic mythic and the movie replica. Ink swords drawn by the film, a silhouette look on the bodies, a rim / back light, an IK pose fix, and its own suspense music beds. | film.cast (any weapon, mythic weight),
film.prop (ink-cast props), look (ink
silhouette, bodyBack), film.posefix,
sound (beds, music kit), rng |
THE OFFERED HAND also has a fire variant in its own file
(PageKills/TheOfferedHandFire.cs). The proof rebuilds the
main one, and the fire variant becomes an engine variant of the same
document once the main one matches.
Every film should also prove its GLOAT hand-off
(film.handoff) and HIGH NOON should be checked on every gun
class whose track ships.
Not covered by these six and left for after launch: a film with a
room rule tied to a real wall or ceiling (13 films, e.g.
the portal film). film.fit has the surface rule from day
one, but its exact match waits for a seventh proof film
[call].
8.4 Skins and cosmetics
| Item | Why | Blocks |
|---|---|---|
| ICE revolver (premium) | The user's own example of a skin carrying sub-features: the look + COLD-BLOODED + ice ammo + ice hit sparks + the ice death burst + the feed icon. | cos.item, cos.art (weapon parts),
cos.premium (all five attachments),
cos.filmrule |
A PLAIN weapon skin (one variant,
Scripts/Render/CosmeticsPlain.cs) |
The look-only recolour, the most common skin we will push. | cos.art (part recolour), palette |
HOODIE outfit (OutfitArt.Hoodie,
CosmeticsCatalog.cs:413) |
A body cosmetic that follows every joint, including in films and
ragdolls, plus a hood that sits behind the head (it must behave right
when a film knocks the head or hat). It replaces the KURTA: the KURTA is
hidden in the game (CosmeticsCatalog.cs:428; the user, 5
Nov: it looks bad, and the game is international). |
cos.art (body rig on joints + head-anchored part),
cos.filmrule |
CROWN hat (HatArt.Crown) |
A head-attached rig that films knock off
(PageKill.HatOff). |
cos.art (head anchor), film interaction |
FIRE aura (AuraArt.Fire) |
An animated layer around the body. | cos.aura, mat.animate |
RAINBOW trail (TrailArt.Rainbow) |
A trail that is drawn from the body's history, not its current pose. | cos.ribbon |
8.5 Taunts and emotes
| Item | Why | Blocks |
|---|---|---|
| LOSER (taunt 1) | Full-body keyframes, face, words, the L-sticker prop, and its gloat that leaves an L that stays. | perf.pose, perf.face,
perf.words, perf.prop, perf.clip,
perf.gloat (mark) |
| CHAI BREAK (taunt 10) | A gloat that walks to and sits on the body, with the clip timed to
the landing (TauntRig.ChaiSitAt). |
perf.gloat (approach + sit), clip sync |
| DAB (emote) | A hands-only wheel emote, the smallest kind. | perf.kind (emote), perf.pose |
| BHANGRA (win dance) | The win-reaction kind. | perf.kind (win dance) |
8.6 Blocks the proof set doesn't reach
The proof set proves that the engine can match the originals exactly. It can't reach every block. These are covered by a block test suite at the gate instead: one small scenario per block, run like the proof, that checks the block's own behaviour, determinism and budget.
- Blocks with no proof item: map pads / lifts / carts / low-g,
map.liquid, the push / magnet / damage hazards, path / pivot / crumble pieces, the legacy hazard flags. - All the §7 new blocks, which have no original to match.
For a block that does have a built-in user (e.g. Seesaw Pier for weight-driven pivots), the test compares against that map's own scenario.
9. Risks
| Risk | What could happen | What we do |
|---|---|---|
| Expressiveness gap | The custom code in 146 film scripts does things no block can say, and new films keep needing "just one hook". | The proof set covers the heaviest scripts (DRIP, COLD-BLOODED, THE OFFERED HAND). At the start of Stage 3 an agent tags every film script's hook body against the block list, and the gaps become blocks or parameters. We accept that some films will wait for an app update with a new block. That is the price of no scripting. |
| "Exact" is hard | Draw-order and float differences between hand code and blocks give tiny frame differences. | Wrapping (not rewriting) the existing code paths, comparing draw lists after quantisation, a per-item diff report, and a decision on how strict exact is (§8.1). |
| Store review | A reviewer reads "downloads animations" as code download. | No scripting by design (§5.15), bundle checks, clear review notes. A first submission with the engine shipping but only built-in content live, then pushes after approval [call]. |
| Determinism across platforms | IL2CPP on ARM vs Mono / IL2CPP on x64 differs in float maths in
sim blocks. |
Replicated state is host-only and sent quantised, so phones never have to agree on float maths. Only gore-independent film state is recomputed and hashed, and it is integer or quantised first (§5.7 rule 3). Golden runs at the gate on Mac, Linux and the reference phones (ARM). |
| New blocks have no original | The ~61 new blocks (§7) can't be "matched", so their bugs are only found by their own tests. | The block test suite (§8.6), the gate's determinism runs, and a first wave of small internal-only packs that use each new block before any live item does. |
| Store review turnaround | A rejection, or a long review of the first build with the engine, moves launch. | Submit the engine build early with only built-in content live. Keep the review notes ready, and budget a resubmission week in the calendar. |
| Join friction | Fetching a missing pack before a join makes friends wait. | Small vector packs, prefetch at app start, packs for a channel kept to a modest set. The room uses the host's set, and the joiner's progress is shown to the room. |
| Download / storage | Sounds grow the cache, old phones are short on space. | Size caps, LRU eviction of unused packs, sounds shared through library packs. |
| Performance on low phones | A heavy pack runs fine on the Mac and stutters on a LOW phone. | Per-block costs and per-tier budgets at compile time, phone-tier
checks before live, the runtime budget meter. |
| Studio scope creep | A full visual editor delays launch for months. | A minimal Studio v1 (§11) plus a CLI-first build order. The import of baked takes keeps the kill-page pipeline as the motion source [ask]. |
| Two catalogues drift | Built-in and engine items behave differently in the shop or pass. | One ContentCatalog interface, with built-ins adapted
rather than duplicated, and one export. |
| Formulas (D12) and app review | A reviewer reads downloaded formulas as code. | The formula set is tiny and bounded (§5.5a), the review notes describe it, and there is one server kill switch to constants, which every item is gated with. |
| A bad pack reaches players | A crash or an offensive string. | Gate + approval + staged rollout + kill list via config, plus the phone's validator dropping invalid docs whole. Rollback is a config version. |
| Signing key | The publish key leaks and someone pushes a pack. | The private key lives only in the gate's secret store on the VM (not the repo, not the Mac). Rotation by an app update carrying two public keys. |
| Load on our machines | The gate's every-arena × every-level × every-tier runs are heavy. | Gate runs on the VM / M2 by the existing lane rules (Mac last, load bands), and per-item caching of unchanged variants. |
10. Build order and sizes (sequential engines)
The user (6 Nov): "u build one by one, first map engine, then
animation engine, like that" [user]. Each engine is
finished end to end, with its blocks, its proof items identical, its
gates, its online play, its Studio editing (agent-first, then visual)
and its playbook, before the next one starts. The full
task list is
docs/superpowers/plans/2026-11-06-content-engine.md.
| Stage | What | Build (lane-days) |
|---|---|---|
| 1 Shared core (only what maps need) | 3 map de-risk spikes; document model + schema generator; pack format + hashes + store; loader / validator / budget meter; logic graph; formula set (map slots); seeded streams; agent CLI; sbapi registry + content / kill lists + approval; content.stickbash.com; splash download | 36-42 |
| 2 MAP engine | today's map blocks + the 23 new map blocks + shared T1 blocks; bots; protocol 145 + relay + online pack checks; bundle lane; proof maps identical; map gate; Studio map editing; playbook. Done: an agent makes a brand-new map from the CLI, previews it, bot-tests it, passes the gates, publishes it to test phones, and it plays online | 50-60 |
| 3 ANIMATION engine | the film runtime spike; film blocks on the page-film engine; the 16 new film blocks; the kill-page → Studio import; 3 gore levels; protocol 146; the 6 proof films identical; Studio film timeline | 42-52 |
| 4 SKIN engine | the shader material feature; cosmetic + the 8 new skin blocks; premium attachments; loadout extension (147); owned-item fallback; the 6 proof cosmetics identical | 22-27 |
| 5 TAUNT / EMOTE engine | performance + the 6 new taunt blocks; taunt ref (148); the 4 proof performances identical | 12-15 |
| 6 Catalogue + polish + launch | the shared catalogue across shop / pass / customize; Studio polish; reference-phone step; store submission; drills; the launch rehearsal | 12-16 |
Total: about 174-212 lane-days (about 200-243 with review rounds). That is about 10 % more than building everything interleaved, the price of finishing one engine at a time.
Calendar:
| Lanes | Map engine done | Launch-ready |
|---|---|---|
| 1 lane | ≈ 14-17 weeks | ≈ 30-36 weeks |
| 2 lanes | ≈ 9-11 weeks | ≈ 19-23 weeks |
| 3 lanes | ≈ 8-9 weeks | ≈ 16-19 weeks |
Protocol: each engine adds its wire messages with one bump (145 maps, 146 films, 147 skins, 148 taunts). This is allowed only because no player builds exist before launch, and the launch build carries the last number [call].
11. What launch needs
Launch waits for the engine, the Studio and the proof set [user]. Concretely:
- All four engines (Stages 1-5, each passing its own
done definition) in the store build:
ENGINE_API = 1, every block family of §5.2 plus the ~61 T1 new blocks (§7, D1), the final protocol number, and the shared catalogue serving built-ins and packs (Stage 6). - The proof set exact (§8.1 report green): 3 maps, 6 films, 6 cosmetics, 4 performances, all on the internal channel only (players keep the originals). Every new block also passes its block test (§8.6).
- The agent interface (Stage 1, grown in each stage; §6) on the VM / M2 / Mac, meeting the < 30 s edit → still target, and the Studio v1 (each engine's editing + the Stage 6 polish) on the Mac. At least one item of each kind is made end-to-end by an agent following the playbook, through gate → approval → the test channel.
- The pipeline live: sbapi
content,content.stickbash.com, the content list and kill list, signing, human-only approval, rollout, and a rollback + cascade drill done. - Store: review notes written, the first submission accepted with the engine in it.
- At least one real new item (a skin or a taunt)
pushed to
testand thenliveafter launch day, to prove "no app update" in production [call].
Not needed for launch: migration of any old content, player creation,
the seventh (room rule) proof film, a second CDN
origin.
12. Left to the builder [call]
- The binary layout of compiled documents (follow
pk_pack2.py's style: varints, string table, deflate). - The exact per-block cost formulas and per-tier budgets, measured in Stages 1-5 on the reference players.
- The Studio's UI layout, built in the game's own look.
- The content repo layout (one folder per pack, one JSON per document, sounds beside them).
DECISIONS FOR THE USER (all DECIDED)
The numbering follows the coordinator's asking order (D1 … D15). All 15 were decided on 6 Nov; the design is APPROVED.
How many new building blocks go into the launch app? Pushed items can only use blocks the app already has. A block we leave out can't be used until the next app update.
- DECIDED (6 Nov): (A), today's blocks plus the ~61 T1 new blocks.
- (A) Recommended: today's blocks plus about 60 new ones (§7, the T1 list). That is about 23 for maps (moving and spinning platforms, switches and doors, portals, lasers, saws, crushers, spikes, falling objects, spawners, rising water and lava, wind and gravity zones, weather, and the wiring that combines them), 16 for films, 8 for skins, 6 for taunts and 8 shared. Launch is about 10-13 weeks away.
- (B) Everything in (A) plus the next 12 (T2: score zones / king of the hill, creatures, x-ray and lens effects, dangling capes and tails, walking taunts and more). Launch moves about 3-4 weeks later.
- (C) A smaller start: today's blocks plus about 5 new ones per kind (around 25). Launch is about 2-3 weeks sooner, and more new ideas wait for app updates.
- (D) Today's blocks only. Launch is about 8-10 weeks away, and anything new needs an app update.
Can an item made by an AI helper reach players without a person approving it? Every item passes all the automatic checks first.
- DECIDED (6 Nov): (A), a person (you or the lead coordinator) approves, and the server enforces it.
- (A) Recommended: a person (you, or the lead AI coordinator on your behalf) looks at the item's check sheet and taps APPROVE before players get it. Helpers may send items to our own test phones on their own. The server enforces this: only a person's dashboard login can approve.
- (B) Passing the checks is enough. Helpers publish to all players themselves.
- (C) Only you approve, item by item.
How complete must the Studio (our Mac creator app) be at launch?
- DECIDED (6 Nov): (A) at launch. Freehand drawing and posing come in a later Studio update, with no data change (§6.7). The user: AI agents will build ~95 % of content, so the agent interface comes first (§6).
- (A) Recommended: it works for all four kinds (maps, films, skins, taunts). You can open an item, try it in a real match against a bot, change any setting, set poses and timing on a timeline, wire map logic, run the checks and publish. No freehand drawing tools yet: shapes are typed in or imported.
- (B) Everything in (A) plus freehand drawing for shapes and a drag-the-stickman posing tool. Launch moves about 2-3 weeks later.
- (C) Only the helpers' typed commands and a preview window at launch. The visual editor comes later.
A friend joins an online room that uses a new map or film they don't have yet. What happens?
- DECIDED (6 Nov): (A) + (C). The app downloads new content at the splash with a progress bar before the menu, continues with cached content if offline, and rooms download a missing pack with a progress bar before joining (§5.13).
- (A) Recommended: their phone downloads it first, then they join. A progress bar shows the wait: a second or two for most items, longer for an item with new sounds.
- (B) The room only picks maps and films everyone already has, so nobody waits.
- (C) They can't join until they download it themselves.
(Their friends' new outfits are never a problem: they download in the background, and a plain default look shows until then.)
When does a phone download new items?
- DECIDED (6 Nov), settled by D4: at app open, on any network, blocking with a progress bar. The size cap of D6 applies.
- (A) Recommended: quietly in the background when the game opens, on any network. Most items are tiny because the art is drawn by the game itself, not pictures.
- (B) Only on Wi-Fi.
- (C) Only when the player opens that item or joins a room that needs it.
How much space may downloaded items take on a phone?
- DECIDED (6 Nov): (A), at most 5 MB per pack and 150 MB in total, with LRU eviction; owned items are always kept.
- (A) Recommended: at most 5 MB per pack and 150 MB in total. Packs not used for a while are deleted and downloaded again if needed. Items a player owns are always kept.
- (B) Tighter: 2 MB per pack, 60 MB in total.
- (C) No limit.
How exact is "exact" for the proof set?
- DECIDED (6 Nov): (A), identical: zero difference per frame in shapes, sounds and positions.
- (A) Recommended: identical. The computer compares every frame's shapes, sounds and positions with the original and finds no difference.
- (B) No difference you can see side by side. Tiny invisible differences are allowed.
- (C) Same look and feel. Small visible differences are allowed.
Are these the right proof items? Maps: Backyard, Belt Line, Wrecking Yard. Films: SIX-SHOOTER, THE X, DRIP, HIGH NOON, COLD-BLOODED, THE OFFERED HAND. Cosmetics: the ICE revolver, one PLAIN skin, the HOODIE outfit, the CROWN hat, the FIRE aura, the RAINBOW trail. Taunts: LOSER, CHAI BREAK, DAB, BHANGRA.
- DECIDED (6 Nov): (A), as listed, with the HOODIE.
- (A) Recommended: yes, as listed.
- (B) Yes, but swap Belt Line for Seesaw Pier (it tests a plank that tips with players' weight).
- (C) Add more films (8-10) for wider coverage. Launch moves about a week later.
How do new items reach players?
- DECIDED (6 Nov): (A), our test phones → 10 % for a day → everyone, with a one-tap pull-back at every step.
- (A) Recommended: our own phones first, then 10 % of players for a day, then everyone. Any step can be pulled back in one tap.
- (B) Our own phones first, then everyone.
- (C) Everyone at once.
Where does the movement of new kill films come from?
- DECIDED (6 Nov): (A), motion from the kill pages; the Studio adds props, camera, sound and the gore levels.
- (A) Recommended: as now, from the kill pages we already make in the browser, brought into the Studio. The Studio adds props, camera, sound and the gore levels.
- (B) Posed directly in the Studio from the start.
- (C) Both from day one. More to build before launch.
Must every new kill film be made at all three gore levels?
- DECIDED (6 Nov): (A), all three gore levels authored.
- (A) Recommended: yes. The maker builds LOW, MEDIUM and EXTREME (as today), and the checks play each one.
- (B) The maker builds EXTREME only, and the game works out MEDIUM and LOW automatically. Faster, but less control over how the lower levels look.
Should content be allowed small formulas? Some effects would be quicker to describe with a formula, for example "glow gets brighter the faster you move".
- DECIDED (6 Nov): (B), the user overrode the recommendation: a tiny bounded formula set (§5.5a) with a server kill switch to constants.
- (A) Recommended: no formulas. Only the fixed blocks, curves and wiring. This is the safest reading of the app-store rules, and a missing ability becomes a new block in an app update.
- (B) Allow a tiny fixed formula set (add, multiply, min, max, no loops). More flexible, with a small app-review risk.
We pull a broken item that players have bought. What do they keep?
- DECIDED (6 Nov): (A), they keep it: it rolls back to the previous good version, or is hidden until the fix if there is none, then returns by itself.
- (A) Recommended: they keep it. It goes back to its previous good version, or is hidden until the fix if it had none, then comes back by itself.
- (B) It is hidden until the fix (still owned), even if an older good version exists.
- (C) We refund their gems as well.
Where are the download files kept for players?
- DECIDED (6 Nov, the coordinator's technical call): (A), our server behind the Cloudflare cache, immutable files. The setup still to do is listed in §5.13.
- (A) Recommended: on our own server, behind Cloudflare's worldwide cache. Files never change once published, so the cache serves almost every download. Low cost, nothing new to set up.
- (B) Cloudflare R2, a paid storage service. Our server does no work for downloads, at a small monthly cost per use.
- (C) Google's storage (Firebase). Works well, but it is a separate bill and another account to manage.
Where do the designs and sounds we make live?
- DECIDED (6 Nov, the coordinator's technical call):
(A), a new private repo
stickbash-content. - (A) Recommended: in their own new private project on GitHub ("stickbash-content"), apart from the game and server code. Helpers can make content without any risk of touching code.
- (B) Inside the server's project.
- (C) Inside the game's project.
- DECIDED (6 Nov, the coordinator's technical call):
(A), a new private repo