Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
37 KiB
LANTERN on Bevy — Greenfield Architecture Guide
Status: thought-experiment / spike reference — NOT an adopted direction. The operative direction remains LANTERN-on-Unity per DR-048_Lantern_Adoption_Full_Pivot; this document exists so the engine question can be evaluated concretely at the one moment it is cheap: while Lantern_World_Model_Spike is still open and no world code exists.
Date: 2026-07-26
Stack: Rust (stable) · Bevy 0.18 · Avian 0.6 (avian3d) · lightyear 0.26.x
Related: Identity_Lantern · Roadmap_Lantern_Slice · Lantern_Strip_Mothball_Inventory · Art_Direction_Lantern
0. What this is / isn't
This is a high-level but detailed map of how LANTERN — co-op action-RPG, light is territory, seed-pinned pocket-graph, SoD manual-aim skillshots, suit-frames + Sparks + mutations, server-authoritative with client prediction — would be architected greenfield on the Rust stack. It covers workspace shape, netcode model, world model, physics, animation, rendering, testing, tooling, milestones, and risks.
It is not a migration plan for the Unity codebase. The premise is greenfield: the Unity repo stays untouched; what ports is knowledge (tuning tables, design rules, scar tissue), not code.
The core thesis (from the engine-fork discussion, 2026-07-26): LANTERN's two biggest unwritten systems — the runtime-generated pocket-graph world and the light-as-territory field — are the systems Unity's bake-centric DOTS pipeline fights hardest and Bevy's runtime-first model fits best. The biggest written system (combat feel) ports as pure data because it is tick-indexed numbers. That asymmetry is what makes this fork worth documenting at all.
1. Design invariants that survive the engine
Everything in this section is engine-agnostic and ports verbatim. Encode these as data + tests in week one; they are the most expensive-to-rediscover assets the project owns.
1.1 The tuning tables (port as literal numbers)
Keep the sim at 60 Hz fixed tick — identical to Unity NFE's default — so every tick-indexed number transfers 1:1 with no re-derivation:
| Knob | Locked value (07-21, 2026-07-21_Heavy_Feel_Lock_F) |
|---|---|
| Melee contact ticks (swing 1/2/3) | +20 / +12 / +25 |
| Recover | 36 ticks · combo grace 28 · input buffer 10 |
| Measured swing gaps | 36 / 36 / 54 |
| Move speed during swing | 0.25× |
| Finisher | hold 7 frames · kick 1.1 · range ×1.25 |
| Cast facing window | 26 ticks (step-3 contact 25 sits ONE tick under it — never raise contact without the window) |
| Reach | 2.2 · axe scale ×1.25 |
| DPS target | 60 |
| Hit react / stagger | react @1.4× / stagger @1.2× speed · poise threshold 50 |
1.2 The design rules (port as conventions + regression tests)
- Rollback discipline: anything in the predicted/fixed-tick path is deterministic, idempotent, wall-clock-free,
Random-free. (lightyear re-runs FixedUpdate on rollback exactly as NFE re-runsPredictedSimulationSystemGroup.) - Derive, don't replicate: any state computable from already-replicated state is computed on both sides, never sent. (Player
DeadfromHealth<=0; and — new, load-bearing — the entire light field from replicated emitter positions. See §7.) - Tick wrap discipline: network ticks wrap (lightyear's
Tickis a wrapping u16, tighter than Unity's u32 — the hazard is worse, not gone). All stored-tick comparison goes through wrap-safe helpers; a defaulted/zero scheduled tick means not-ready → skip/lazy-stamp, never fire (the 07-21 storm class). - Dual timelines: predicted entities live ~RTT/2 ahead of the server; interpolated entities live behind it. A visual cue anchored to an interpolated entity's own effect must use the interpolation timeline, not the predicted one (the ZoneTelegraph lesson — lightyear exposes both ticks explicitly via its sync manager, so this rule has a first-class home).
- Swept, not point: every hit/area test covers the segment traversed this tick; cover with a tunnelling regression test from day one.
- One-press-one-action: input abstractions accumulate/stick in every netcode stack. Gate one-shot actions on a press edge relative to the previous tick's input, and write the "one tap = one cast across a cooldown reopen" regression test before shipping the first ability (the 07-21e bug, now automatable headless — see §12).
- Honesty: a telegraph is the truth of the sim (arc-reveal ends at contact; fill direction encodes the actual schedule). Single-source-of-truth structures (§7) make this by construction rather than by audit.
- Soft-fail resource spends: afford→act else soft-fail with no cooldown burn; read the ledger live in-loop; commit withdraw + reservation atomically inside the request handler.
- Grid math: corner-origin, center-returning, half-open cell bounds, floor(); lock cell/plot size as a coordinate space once.
- Interest-management re-audit: whenever a second region/room shares an existing tag or query, re-audit every global query over that tag (the DR-031/040 class — rooms don't remove this failure mode, they relocate it).
- Present forks, don't auto-decide; operator gates state Changed: + Looking for:. Process rules port too.
1.3 What deliberately does NOT port
- All Unity plumbing knowledge: Burst hazards, asmdef/source-gen rules, subscene baking,
.metadiscipline, MCP editor-bridge workarounds, Rukhanka graft math, Entities-Graphics deformation shaders, URP asset versioning. On this stack those categories cease to exist (they are replaced by a smaller, different set — see §15). - The prior quarry verdicts in Lantern_Strip_Mothball_Inventory — greenfield means the quarry is design, not code.
2. Stack and version matrix
The pinning rule: lightyear's supported Bevy version is the constraint solver for the whole matrix. Pin everything in the workspace Cargo.toml, upgrade as a deliberate scheduled task (never ambiently), and treat a Bevy minor bump as a milestone-sized event with a migration-guide read.
| Crate | Version (as of 2026-07) | Role | Notes |
|---|---|---|---|
bevy |
0.18 (Mar 2026) | engine | ~quarterly breaking releases; no LTS |
avian3d |
0.6 (Mar 2026) | physics | ECS-native; 0.6 adds built-in move-and-slide (the character controller problem has a first-class answer now), joint motors, BVH |
lightyear |
0.26.x | netcode | server-authoritative replication, client prediction + rollback, interpolation, input redundancy, rooms; ships an Avian integration crate — this junction is the most version-sensitive point in the stack, verify it first at spike time |
leafwing-input-manager |
latest compatible | input | lightyear has native integration; action-state model |
bevy_egui + bevy-inspector-egui |
latest compatible | dev tooling | the inspector/tuning-bench substrate |
serde + ron |
stable | data | profiles, spark/mutation defs, gen params |
rand + rand_chacha |
stable | determinism | seeded, portable RNG streams |
petgraph |
stable | pocket graph | graph gen + queries |
bevy_hanabi |
latest compatible | GPU particles | murk particulate, hit effects |
bevy_kira_audio (or stock bevy_audio) |
latest compatible | audio | procedural clip generation possible |
insta, proptest |
stable | testing | snapshot-pin worldgen; property tests |
tracing |
stable | logging | structured, greppable |
Fallback noted up front: if lightyear stalls (single-maintainer risk), bevy_replicon + hand-rolled prediction is the documented retreat. §3's crate boundaries are drawn so that retreat replaces one crate, not the game.
3. Workspace layout
Cargo workspace; the Unity asmdef split maps cleanly, with one upgrade — a zero-Bevy core crate that holds the most valuable logic:
lantern/
├── Cargo.toml # workspace + pinned version matrix
├── crates/
│ ├── lantern_core/ # ★ NO bevy/lightyear/avian deps. Pure Rust:
│ │ ├── worldgen/ # seed → PocketGraph (petgraph), gen invariants
│ │ ├── lightfield/ # grid field math, emitter → field computation
│ │ ├── tickmath/ # wrap-safe tick compare, schedule sentinels
│ │ ├── gridmath/ # half-open cells, corner-origin (ports BaseGridMath)
│ │ ├── combat/ # cadence tables, damage/poise math, cone/segment tests
│ │ └── tuning/ # FeelProfile types (serde), code defaults
│ ├── lantern_protocol/ # components + lightyear registration: channels,
│ │ # messages, inputs, prediction/interp modes
│ ├── lantern_sim/ # shared FixedUpdate systems (movement, melee,
│ │ # abilities, enemies, light-territory ticks)
│ ├── lantern_client/ # render, input gather, presentation, HUD, audio, VFX
│ ├── lantern_server/ # spawning, AI decisions, waves, persistence, validation
│ └── lantern_tools/ # CLIs: seed-gallery, feel-probe, gen-audit, screenshot
├── src/main.rs # launcher: --server | --client <addr> | --host (both)
├── assets/
│ ├── models/ # glTF (Blender-native export)
│ ├── profiles/ # RON feel profiles (hot-reload)
│ ├── content/ # sparks, mutations, frames, enemy defs (RON)
│ └── shaders/ # WGSL, in-repo, hot-reload
└── tests/ # integration: server + N clients in-process
Mapping from the Unity split: ProjectM.Simulation → lantern_core + lantern_sim; ProjectM.Client → lantern_client; ProjectM.Server → lantern_server; ProjectM.Authoring → deleted as a concept — there is no bake step; authoring is RON data + spawn functions.
Why lantern_core matters most: it compiles in seconds with zero engine deps, so worldgen/combat/tick math get sub-second test cycles, are immune to Bevy's quarterly churn, and survive even an engine retreat. It is the project's actual IP; everything else is adapters.
4. App architecture
4.1 Plugins, schedules, states
- Plugins are the feature unit (mirror of the Unity feature folders):
WorldGenPlugin,LightFieldPlugin,MovementPlugin,MeleePlugin,AbilityPlugin,EnemyPlugin,WavePlugin,HudPlugin,MurkRenderPlugin,DevToolsPlugin. Client/server bins compose different plugin sets over the samelantern_simmiddle. - All gameplay simulation runs in
FixedUpdateat 60 Hz (lightyear's tick duration is set to match:1.0/60.0). This is the rollback boundary — lightyear re-runs FixedUpdate on misprediction, so every §1.2 rollback rule applies to everything scheduled there. - Presentation runs in
Update(render-rate, once per frame, never rolled back) and is observe-only — thePresentationSystemGroupdiscipline ports wholesale: presentation systems read sim state, own their caches, prune them viaRemovedComponents(despawn detection is first-class — the "prune the cache each frame" pattern gets engine support), and never mutate sim state. - Explicit ordering via
SystemSets with.configure_sets— e.g.SimSet::{Input, Movement, Combat, Cleanup}chained. Two upgrades over Unity: Bevy's ambiguity detector can be enabled in CI to flag unordered read/write pairs, and ordering cycles surface when the schedule is built in tests (see §12), not only at Play-time world creation. - Game flow via
States:Menu → Connecting → Loading → InGame. State-scoped entities/systems replace the entire menu-world / world-lifecycle dance (DR-019_Frontend_Menu_Settings_Saves_Build's hardest-won section becomes ~free).
4.2 Session topology
--server: headless bin,MinimalPlugins+ server set. Small native binary, fast boot, cheap to host — dedicated co-op servers become economically trivial, and CI can boot real servers per-test.--client <addr>: full render client.--host: single app runningClientPlugins+ServerPluginstogether (lightyear supports both plugin sets in one app). This is single-player and listen-server co-op in one code path — no on-demand world creation/disposal, no frame-boundary coroutine hazards.- Transport: UDP native; WebTransport for wasm — a browser-playable client is a real option (playtests become a link), something the Unity stack effectively cannot do.
4.3 ECS concept map (Unity DOTS → Bevy)
| Unity DOTS / NFE | Bevy / lightyear | Notes |
|---|---|---|
IComponentData struct |
#[derive(Component)] |
no unmanaged/managed split to manage |
DynamicBuffer<T> |
Vec<T> field, or child entities via relationships |
pick per access pattern |
IEnableableComponent |
bool field (hot toggles) or marker insert/remove (archetype move) | insert/remove is Bevy's structural change — same batching instincts apply |
ISystem + Burst |
plain fn system |
native codegen; no Burst ICEs, no source-gen, no *.g.cs — that entire CLAUDE.md section dies |
EntityCommandBuffer |
Commands |
applied at sync points; same "batch structural changes" instinct |
SystemAPI.Query (7-arg cap) |
Query<(…)> (larger tuple cap, QueryData derive beyond) |
the ComponentLookup workaround class mostly disappears |
| Singleton component | Resource |
TuningConfig, run info, ledger |
| Baker / Authoring / subscene | RON asset + spawn function | no bake, no .meta, no re-bake rules |
SystemGroup + [UpdateBefore/After] |
Schedules + SystemSet + .before/.after/.chain |
ambiguity detection available in CI |
5. Netcode model (lightyear)
5.1 Concept map
| Unity NFE | lightyear 0.26 | The ported scar |
|---|---|---|
Ghost + [GhostField] |
component registered in the protocol with a replication + sync mode | registration is code in lantern_protocol, greppable, no authoring component |
| Predicted ghost (owner) | client-side Predicted entity mirror; FixedUpdate re-runs on rollback (RollbackPolicy { state, input, max_rollback_ticks }) |
all §1.2 rollback rules |
| Interpolated ghost | client-side Interpolated mirror on the delayed timeline | interpolated-tick cue rule (§1.2 dual timelines) |
IInputComponentData / InputEvent |
lightyear input plugin (native or leafwing ActionState); inputs auto-sent with N-tick redundancy against loss |
one-press-one-action edge gating + regression test, day one |
IRpcCommand |
message on a reliable channel | payloads plain serde structs; server validates; atomic commit in handler |
GhostRelevancy SetIsIrrelevant |
Room membership (clients + entities sets) |
default-relevant-unless-hidden posture ports: global/untagged state lives outside rooms; re-audit rule (§1.2) |
NetworkTick (u32) |
Tick (wrapping u16) |
wrap discipline is MORE critical, not less |
| Prediction/interpolation tick split | sync manager: prediction time ~RTT/2 ahead, interpolation time behind | first-class home for the ZoneTelegraph lesson |
5.2 LANTERN's replication diet
The design goal: replicate dramatically less than a generic game, because LANTERN's world is a pure function of a seed.
- World = seed.
(u64 seed, GenParams version)sent once at join. Both sides runlantern_core::worldgen; the server spawns sim entities (colliders, spawners, nodes), the client spawns visuals. Environment replication: zero bytes. - Light field = derived. Never replicated (see §7); computed on both sides from the replicated emitter set.
- Players — predicted: position/velocity (via the Avian integration), facing yaw, health, socket/ability state, melee combo state. The
PlayerFacingmodel ports exactly: body-yaw only, every gameplay direction through aresolve_aim(input_aim, facing)inlantern_core. - Enemies/projectiles — interpolated, minimal: position, health, attack-state byte, kind byte. Client derives animation (§10) exactly as
EnemyAnimationDriveSystemdoes today. - Run/meta singletons — resources ledger, run info: replicated resource or singleton entity on a reliable channel; lives outside rooms (the untagged-director-ghost rule, ported).
- One-shot actions — build placement, deposits: reliable messages, server-validated, atomic afford→commit in the handler.
Rooms map to pockets (or pocket clusters) for interest management once the world outgrows one arena; global state stays room-less.
6. World model: seed-pinned pocket-graph
This is the section Unity was going to fight and Bevy doesn't.
- Pure generator:
lantern_core::worldgen::generate(seed: Seed, params: &GenParams) -> PocketGraph— petgraph graph of pockets (nodes: shape, biome-flavor, light budget, spawn tables) and corridors (edges: width, length, gates). No engine types anywhere in it. - Determinism rules:
ChaCha8Rngstreams forked per subsystem (layout,population,loot— so adding a loot roll doesn't reshuffle layout); noHashMapiteration-order dependence (BTree or sorted collections in gen paths); prefer integer/cell coordinates in structural decisions. - Generator versioning:
GenParamscarries a version; a change to generation logic bumps it and is a new epoch for seeds — the exact discipline already used forSaveDataversions (DR-019_Frontend_Menu_Settings_Saves_Build). Canonical seeds are snapshot-pinned withinstaso accidental drift fails CI. - Invariants enforced by the generator, tested by property tests — this is the greenfield superpower: guarantees at gen-time replace runtime hacks. Concretely:
- full connectivity (every pocket reachable) across N-thousand random seeds;
- corridor min-width ≥ k × character radius — this deletes the frozen-Husk bug class at the source (the Depenetrate/tangent-slide/nudge chain existed because hand-placed cover created unnavigable wedges; a generator can simply never emit them);
- spawn points depenetrated by construction (no embedded spawns);
- light-budget and pacing distributions inside designed bands (Monte-Carlo balance in
cargo test).
- Runtime spawning: server iterates the graph → spawns Avian static colliders + gameplay entities; client iterates the same graph → spawns meshes, murk volumes, decor (seeded decoration RNG, so co-op clients see identical worlds without replicating a single prop).
- Operator eyes-on:
lantern_tools seed-gallery --count 50renders top-down maps to PNGs for review — the browse-many-seeds loop the design (Identity_Lantern) will need, impossible to do cheaply through an editor.
7. The light field — light is territory as one data structure
The core mechanic gets a single source of truth shared by sim and renderer.
- Representation: per-pocket coarse grid (
0.5–1 mcells,u8orf32intensity), stored inlantern_core::lightfield. Computed as a pure function of the emitter set: lanterns, placed lures, spark glows, mutation auras — each(position, radius, falloff, flicker-seed). - Derive, don't replicate: emitters are already-replicated entities; both server and client run the same field computation in FixedUpdate. Zero bandwidth, rollback-correct (the field re-derives on rollback because it is a function of rolled-back state), and server and screen cannot disagree — the honesty class of bugs (socket honesty, arc honesty, zone-fill timing) is prevented structurally rather than audited per-feature.
- Gameplay queries:
field.light_at(pos) -> f32drives territory ticks, enemy courage/aggression curves, mutation triggers, harvest eligibility — all inlantern_simFixedUpdate systems. - Rendering: the same grid is uploaded per-frame as a texture/storage buffer into a custom murk pass — a fullscreen render-graph node (or extended material) that darkens toward the field's inverse, with WGSL handling flicker, edge glow, and particulate density. The
EmissiveGloamidea ports directly as a WGSL material — and on this stack, hand-written shader files in-repo are the norm, hot-reloaded on save, not a workaround for un-scriptable ShaderGraphs. - Cosmetic light layer: actual Bevy point lights on emitters for local highlights — clustered forward handles many small dynamic lights, which is precisely the load profile of a lantern game (sparks, lures, glows everywhere).
8. Physics (Avian) — sized to the game, with an escape hatch
- World collision: static Avian colliders spawned from pocket-graph geometry at runtime (hulls/boxes per wall segment — generated, so they are the visual truth; the entire
ColliderFitToolsaudit-and-refit problem class disappears). - Character: kinematic body + Avian 0.6's built-in move-and-slide — the collide-and-slide controller that needed a dedicated Unity package now ships in the physics crate.
LockedAxespins rotation (theFreezeRotation-not-honored gotcha has a first-class answer). Top-down config ports: no ground snap, rotation owned by the facing system, zero gravity feed (underwater). - Enemies: kinematic circles with the same move-and-slide; no pathfinding at first — but note the §6 point: generator-guaranteed corridor widths remove the main reason enemy sliding ever soft-locked.
- Hits: Avian
SpatialQuery(ray/shape casts) for all swept segment/cone tests; the swept-not-point rule and its tunnelling regression test port unchanged. - Rollback integration: lightyear's Avian integration crate handles physics-state rollback for predicted entities. This is the single most version-sensitive junction in the stack — it is the first thing the M0 spike must validate (§14).
- Determinism scope: Avian is not cross-platform bit-deterministic. That is fine for this architecture (snapshot server-authority, same as Unity Physics today) — but it forecloses lockstep; don't design anything that assumes bit-identical simulation across machines.
- Escape hatch (deliberate): all gameplay collision queries go through a thin
lantern_sim::physicsfacade. If Avian rollback cost or behavior bites, the facade's alternate backend is a bespoke deterministic 2.5-D layer (circles vs. segments over the pocket graph — a few hundred lines, property-testable). The game's actual needs are modest enough that this is a real option, and the facade keeps it a swap instead of a rewrite.
9. Combat port
The most valuable existing work, and the cheapest to port — it is tick-indexed data plus small systems.
- Cadence tables (§1.1) land in
lantern_core::combatas constants +FeelProfileoverrides. At 60 Hz they are correct on arrival. - Damage-at-contact: the
MeleeCleavePendingschedule-and-consume pattern ports as a component + FixedUpdate system: stampcontact_tickat swing start (wrap-safe, §1.2 sentinel rules), consume when reached, resolve the swept cone via the physics facade, apply damage + poise, emit cue state. - Event caution (new-stack gotcha, flagged now): Bevy's frame-buffered
Events interact badly with rollback re-runs — a rolled-back FixedUpdate can double-emit or lose them. Sim-authoritative "events" (damage, deaths) are components/queues on entities, drained by consuming systems; renderer-facing cues read state changes. Verify current lightyear guidance at spike time; until then, noEventWriterinside rolled-back systems. - Buffered input, combo grace, dash-cancel rules: port as written — they are tick logic, and (§12) each ships with the headless regression test the Unity version could only get via live-server probing.
- Feel profiles:
assets/profiles/*.ron, hot-reloaded by the asset server, overlaid via an egui panel with A/B buttons —FeelProfileServicerebuilt in its native idiom. Same rule as 07-20c: profiles are experiments; winners fold intolantern_coredefaults. - The probe becomes a test:
CombatProbe(server-measured per-swing start/contact/damage ticks, cadence, DPS) is reimplemented as an integration test that scripts inputs against a headless server and asserts the locked numbers. Feel regressions fail CI instead of waiting for an operator session.
10. Animation
The one area that gets harder, plus one that gets much easier.
- Pipeline: Blender → glTF (Bevy's native format; Blender's best-supported export — the FBX/Humanoid/
CreateFromThisModelgauntlet disappears). Clips load by name from the glTF;AnimationGraph+ transitions replace the AnimatorController. - The loss — no Humanoid retargeting. Unity's muscle retarget let one clip library serve every rig; Bevy clips are per-skeleton. Mitigations, in order: (1) small rig roster — player frame + a handful of enemy skeletons, a real constraint to hold; (2) Blender-side retarget-and-bake automation — the existing Blender MCP scripting (blender-mcp-and-unity-mcp-v10) becomes a batch script: shared action library → apply per rig → bake → per-rig glTF. The clip library stays single-source; the bake fans out mechanically.
- The win — attachments are trivial. Bevy skeleton bones are plain entities: the salvage axe is a child entity of the hand-bone entity with a local offset. The entire 07-16 graft recipe — rigid-skinning, bindpose rebasing, scale-stripping, tangent recalculation — is replaced by
commands.entity(hand_bone).add_child(axe). The grip-anchor tuning flow survives as a bone-local offset in RON. - Client-derived animation ports as-is: a client-only Update system reads replicated state (velocity from transform deltas for interpolated entities, combat state bytes) and drives graph parameters —
PlayerAnimationDriveSystem/EnemyAnimationDriveSystem, same shape, no replication. Hit reacts keep the windup-honesty gate (light react only when not mid-attack) and the poise threshold.
11. Rendering, presentation, UI
- The LANTERN look ports as a specification (Art_Direction_Lantern): deep-water clear color, exponential teal murk (now field-driven, §7), flat dim ambient, ACES-family tonemapping (Bevy ships filmic tonemappers), bloom for gloam accents, vignette. One look, one config module —
ScenePolicybecomes a plugin choice, not a scene-name check. - Shaders: all WGSL text in
assets/shaders/, hot-reloaded. Extended/custom materials for gloam-emissive and murk-aware surfaces; skinned meshes work with custom materials without the deformation-graph dance. - VFX:
bevy_hanabiGPU particles (murk particulate, bubble trails, contact bursts); smear/ribbon meshes generated in code as today. - Juice discipline ports wholesale: observe-only Update systems, per-frame cache pruning via
RemovedComponents, despawn-inferred death VFX, hit-stop as camera punch never timescale, procedural audio fallback (kira supports raw sample buffers). - UI: code-built
bevy_uifor the HUD (health, ability bar with drain overlays/countdowns/ready flashes — the AbilityBar spec ports);bevy_eguifor every dev surface (tuning bench, inspector, seed browser, debug ops). The UITK trap list (PanelSettings/EventSystem/theme wiring) has no equivalent; bevy_ui brings its own smaller quirks (focus/interaction model) — expect a new, shorter gotcha list.
12. Testing & CI — the payoff section
This is where the stack pays for itself; roughly five ★-class Unity limitations become ordinary tests.
- Pure-core tests (
lantern_core): grid math, tick wrap, worldgen invariants, combat tables — sub-second, no engine. - Schedule-true app tests: build a real
AppwithMinimalPlugins+ sim plugins, tickFixedUpdatemanually, assert. Because the actual schedule is constructed, system-ordering cycles and ambiguities surface in tests — the "invisible to EditMode tests, throws only at Play world-creation" class dies. Enable Bevy's ambiguity detector in CI. - In-process netcode tests: server app + N client apps over in-memory/local transport in a single
cargo test— the capabilityNetCodeTestWorld is internalwalled off. Day-one regression suite: one-press-one-cast across a cooldown reopen (the 07-21e bug, with synthetic inputs through the real input path); buffered-attack edge validity; join-mid-run replication; room-transition relevancy. - Determinism harness: run identical seed + scripted inputs twice, hash sim state per tick, assert equal. Wall-clock leaks, unseeded RNG, and iteration-order nondeterminism get caught mechanically — today this is enforced only by review discipline.
- Feel-probe tests (§9): locked cadence numbers asserted headless.
- Worldgen property + snapshot tests (§6): thousands of seeds per CI run.
- CI:
cargo fmt --check·clippy -D warnings· full test suite · wasm build check — all headless, no editor, no GPU required for the core suite. (Rendered-screenshot tests exist but are CI-flaky; keep them optional/local.)
Workflow consequence for agent-driven development: the entire verify loop — compile, unit, netcode, determinism, feel — is CLI-only. No editor bridge, no focus requirements, no stale-assembly hazards, no Play-mode-only truths. An agent can take a combat change from edit to "cadence still 36/36/54, determinism holds, one-press regression green" without a human or an editor in the loop.
13. Dev workflow & tooling
- Inner loop:
cargo checkfor seconds-fast validation; dev profile withbevy/dynamic_linking+ a fast linker for quick rebuild-and-run;opt-levelbumped for deps so debug play is smooth. Full clean builds are slow (Rust tax) — cache in CI, never clean locally without cause. - Hot reload: RON profiles, WGSL shaders, and glTF assets reload on save via the asset watcher — tuning and look iteration without restarts. (Code hot-patching exists as experimental ecosystem work; treat as a bonus, not a plan.)
- Live-world introspection (BRP): Bevy ships the first-party Bevy Remote Protocol — a JSON-RPC layer into a running app, headless server included. Community MCP servers (
bevy_brp_mcp,bevy_debugger_mcp) sit thinly on top: query/watch/mutate live components, trigger events, launch apps. This is the agent's runtime eyes, and it is protocol-native rather than editor-automation — it works against a dedicated server or any dev build, with no editor session, focus state, or compile-freshness hazards. Enable thebevy_remotefeature in dev builds from day one; unlike the Unity bridge, this bridge is optional (the primary loop stays CLI + text), which is the structural reason the MCP scar list stays short. - Operator surfaces (replacing the editor, honestly the biggest workflow cost — budget for it):
--host+ egui: inspector, tuning bench with profile A/B, debug-op buttons (theDebugOpbyte model ports as reliable messages), F-key toggles;lantern_tools seed-galleryfor worldgen review;lantern_tools staging(orbit-cam scene viewer) as the ArtStaging replacement;- screenshot-to-file on demand for async eyes-on gates — operator gate protocol (operator-gates-state-change-and-ask) unchanged.
- Content-as-data design loop: sparks/mutations/frames/enemy defs in
assets/content/*.ron. An agent can generate a candidate mutation, run it through a headless Monte-Carlo gauntlet (N seeded runs → TTK/DPS/survival distributions), and present measured results at the fork — the tuning-session-defaults-then-adjust cadence, with simulation replacing much of the guesswork. - Docs/knowledge layers unchanged: vault + DRs + session logs + basic-memory all port as-is; a new repo gets a fresh CLAUDE.md seeded from §16.
14. Milestones (gates, not dates — operator owns scheduling)
Each milestone ends at an operator gate with explicit Changed: / Looking for:.
- M0 — the spike, doubled. Build Lantern_World_Model_Spike in Bevy, in a fresh repo, Unity untouched. Scope: seed → pocket-graph → runtime colliders → light field → murk render → two clients over localhost walking pockets, light revealing territory, one melee swing with damage-at-contact + probe test. Must-validate list: lightyear↔Avian rollback integration (§8), input edge semantics (§5), event-vs-rollback pattern (§9), wasm build compiles. Gate: world-model design review (already owed) plus the engine verdict — the spike answers both questions for one price. If either fails, the loss is one spike and the design learnings transfer back to Unity.
- M1 — combat-feel parity. Target dummy, three-swing cadence + finisher, hit reacts, feel profiles, probe tests asserting §1.1 numbers. Gate: operator eyes-on — does the locked feel survive translation?
- M2 — enemies + waves + territory. Enemy kinds on the drive-by-derived-state pattern, wave direction, light-field-driven aggression, pocket rooms/interest management.
- M3 — sockets, sparks, mutations. Content-as-RON, ability archetypes on the resolve-aim model, gauntlet-sim balance harness.
- M4 — co-op hardening + reach. Join-mid-run, disconnect/rejoin, dedicated-server deploy, wasm playtest build shared as a link.
- M5 — look lock. Murk pass, gloam materials, particulate, audio bed; ArtStaging-equivalent review via the staging tool.
15. Risks & mitigations (honest ledger)
| Risk | Severity | Mitigation |
|---|---|---|
| lightyear bus factor (essentially one maintainer) | High | protocol isolated in lantern_protocol; documented bevy_replicon+DIY-prediction retreat; M0 validates the riskiest junction first |
| Bevy quarterly breaking releases vs. LLM training-data lag | Medium-High | pinned matrix, deliberate upgrade tasks, migration guides read at upgrade time; docs fetched at code-time (the context7 rule ports verbatim) |
| Avian↔lightyear rollback integration version coupling | High (localized) | first item on the M0 must-validate list; physics facade (§8) bounds the blast radius |
| No editor for the operator — eyes-on art/feel loop degrades | High (workflow) | budgeted egui/staging/seed-gallery tooling (§13); screenshot gates; accept this is the biggest real cost of the switch |
| No animation retargeting | Medium | small rig roster + Blender batch retarget-bake automation (§10) |
| New scar set (borrow checker in system params, WGSL debugging, rollback-event traps) | Medium | it replaces a larger documented scar set; start the new gotcha ledger at M0, day one |
| Rewrite forfeits 430 green tests + shipped systems | Accepted premise | operator explicitly not attached; §1 ports the knowledge; M0-before-commitment structure keeps the reversible option open |
| Rust ramp for the operator | Low-Medium | operator's surface is RON data, egui panels, and Blender — not Rust internals; agents own the Rust |
16. Day-one conventions (CLAUDE.md seed for the new repo)
- Sim logic lives in
FixedUpdateonly; presentation inUpdate, observe-only. - No wall clock, no unseeded RNG, no
HashMap-order dependence anywhere inlantern_coreorlantern_sim; the determinism harness is the enforcement. - Derive, don't replicate — if it's a function of replicated state, compute it on both sides.
- Ticks are wrapping; compare only through
tickmath; zero/default stored tick = not-ready, never fire. - No Bevy
Events in rolled-back systems; sim events are components/queues. - All hits swept; every new hit path ships with a tunnelling test.
- One-shot actions gate on press edges; every ability ships with a one-press-one-action netcode test.
- Every knob: default in
lantern_core, override in a RON profile; profiles are experiments, winners fold into defaults. lantern_corenever imports bevy/lightyear/avian — enforce with a CI dep-check.- Version-pin the matrix; engine/netcode upgrades are scheduled tasks with migration-guide reads, never drive-by bumps.
- Generator changes bump
GenParamsversion; canonical seeds are snapshot-pinned. - Present gameplay forks to the operator with Changed: / Looking for:; feel gates get measured probe numbers alongside the ask.
Appendix A — what happens to the current CLAUDE.md, by section
| Unity CLAUDE.md section | Fate on this stack |
|---|---|
| Assemblies/asmdefs/source-gen | dies (crate graph + rustc replace it) |
| Burst hazards ★ | dies (no Burst; native codegen, honest compile errors) |
| Netcode/prediction ★ | ports as principles (§1.2, §5) — the single most transferable section |
| Physics & CC | shrinks — swept-hit + config rules port; bake/collider-fit/nudge hacks die (§6, §8) |
| Build/structures/grid | ports (grid math, atomicity, soft-fail — §1.2) |
| Presentation/juice | ports wholesale (§11) |
| Art import (HDRP→URP) | dies (glTF-native pipeline; look ports as a spec) |
| Aim/facing (SoD) ★ | ports verbatim (resolve_aim in lantern_core) |
| Animation (Rukhanka) ★ | half dies (graft/deformation math gone), half ports (drive-by-derived-state), one new cost (per-rig clips) |
| MCP/editor workflow ★ | dies (the CLI is the workflow) — replaced by ~10 lines of cargo conventions |
| Testing | upgrades (§12 — the internal-test-world and Play-only-validation ceilings lift) |
Sources consulted 2026-07-26: lightyear 0.26.4 docs (docs.rs — plugin/tick setup, Room, RollbackPolicy, input redundancy, sync manager), Avian releases (0.6 move-and-slide, Mar 2026), Bevy 0.18 release notes (Mar 2026).