Files
Project-M/Docs/Vault/03_Design/Lantern_Bevy_Greenfield_Guide.md
kronic aa5247aff4 Docs: commit basic-memory frontmatter backfill + resolved MCP lock hash
The 13 vault notes were left dirty by basic-memory back-filling
title/type/permalink frontmatter into files written with Write/Edit
(it also strips the trailing newline — restored here so future diffs
stay clean). Content is unchanged; verified frontmatter-only.

packages-lock.json is the com.coplaydev.unity-mcp #main branch ref
silently re-resolving. Pinning that dependency lands with the package
cleanup batch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 10:09:39 -07:00

39 KiB
Raw Permalink Blame History

title, type, permalink
title type permalink
Lantern_Bevy_Greenfield_Guide note gamevault/03-design/lantern-bevy-greenfield-guide

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-runs PredictedSimulationSystemGroup.)
  • Derive, don't replicate: any state computable from already-replicated state is computed on both sides, never sent. (Player Dead from Health<=0; and — new, load-bearing — the entire light field from replicated emitter positions. See §7.)
  • Tick wrap discipline: network ticks wrap (lightyear's Tick is 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, .meta discipline, 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.Simulationlantern_core + lantern_sim; ProjectM.Clientlantern_client; ProjectM.Serverlantern_server; ProjectM.Authoringdeleted 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 same lantern_sim middle.
  • All gameplay simulation runs in FixedUpdate at 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 — the PresentationSystemGroup discipline ports wholesale: presentation systems read sim state, own their caches, prune them via RemovedComponents (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 running ClientPlugins + ServerPlugins together (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.

  1. World = seed. (u64 seed, GenParams version) sent once at join. Both sides run lantern_core::worldgen; the server spawns sim entities (colliders, spawners, nodes), the client spawns visuals. Environment replication: zero bytes.
  2. Light field = derived. Never replicated (see §7); computed on both sides from the replicated emitter set.
  3. Players — predicted: position/velocity (via the Avian integration), facing yaw, health, socket/ability state, melee combo state. The PlayerFacing model ports exactly: body-yaw only, every gameplay direction through a resolve_aim(input_aim, facing) in lantern_core.
  4. Enemies/projectiles — interpolated, minimal: position, health, attack-state byte, kind byte. Client derives animation (§10) exactly as EnemyAnimationDriveSystem does today.
  5. 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).
  6. 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: ChaCha8Rng streams forked per subsystem (layout, population, loot — so adding a loot roll doesn't reshuffle layout); no HashMap iteration-order dependence (BTree or sorted collections in gen paths); prefer integer/cell coordinates in structural decisions.
  • Generator versioning: GenParams carries a version; a change to generation logic bumps it and is a new epoch for seeds — the exact discipline already used for SaveData versions (DR-019_Frontend_Menu_Settings_Saves_Build). Canonical seeds are snapshot-pinned with insta so 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 50 renders 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.51 m cells, u8 or f32 intensity), stored in lantern_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) -> f32 drives territory ticks, enemy courage/aggression curves, mutation triggers, harvest eligibility — all in lantern_sim FixedUpdate 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 EmissiveGloam idea 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 ColliderFitTools audit-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. LockedAxes pins rotation (the FreezeRotation-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::physics facade. 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::combat as constants + FeelProfile overrides. At 60 Hz they are correct on arrival.
  • Damage-at-contact: the MeleeCleavePending schedule-and-consume pattern ports as a component + FixedUpdate system: stamp contact_tick at 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, no EventWriter inside 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 — FeelProfileService rebuilt in its native idiom. Same rule as 07-20c: profiles are experiments; winners fold into lantern_core defaults.
  • 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/CreateFromThisModel gauntlet disappears). Clips load by name from the glTF; AnimationGraph + transitions replace the AnimatorController.
  • Graph capabilities (verified vs 0.18): the built-in AnimationGraph is a DAG of clip / blend / add nodes with per-node weights and bone-mask groups — velocity-driven locomotion blending, per-state playback speeds (the 1.0/0.85/0.88 retimes port), the additive hurt-flinch layer (open follow-up item — first-class here), and partial-body overlays with explicit mask semantics (vs the Rukhanka un-keyed-bone collapse trap). State machines via AnimationTransitions or bevy_animation_graph (which also ships a visual graph editor). Bone entities carry real deformed-pose Transforms and clips are inspectable data — stride footsteps, grip-anchor verification, and TuningAuditTools-style contact-truth audits become queryable test assertions (the Rukhanka bind-AABB readback lie has no equivalent). Still absent: built-in IK (currently unused) and root motion (already off — CC owns the transform).
  • 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 — ScenePolicy becomes 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_hanabi GPU 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_ui for the HUD (health, ability bar with drain overlays/countdowns/ready flashes — the AbilityBar spec ports); bevy_egui for 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.

  1. Pure-core tests (lantern_core): grid math, tick wrap, worldgen invariants, combat tables — sub-second, no engine.
  2. Schedule-true app tests: build a real App with MinimalPlugins + sim plugins, tick FixedUpdate manually, 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.
  3. In-process netcode tests: server app + N client apps over in-memory/local transport in a single cargo test — the capability NetCodeTestWorld is internal walled 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.
  4. 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.
  5. Feel-probe tests (§9): locked cadence numbers asserted headless.
  6. Worldgen property + snapshot tests (§6): thousands of seeds per CI run.
  7. 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 check for seconds-fast validation; dev profile with bevy/dynamic_linking + a fast linker for quick rebuild-and-run; opt-level bumped 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 the bevy_remote feature 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 (the DebugOp byte model ports as reliable messages), F-key toggles;
    • lantern_tools seed-gallery for 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 + rig/look proof. The fork's weakest leg (animation/visuals) gets judged here, early and explicitly. Scope: Bathynaut rig ported (Blender→glTF, per-rig clip bake), three-swing cadence + finisher on the real rig, axe as bone-child attachment, hit reacts, target dummy, feel profiles, probe tests asserting §1.1 numbers — staged in a murk vignette with volumetric god-rays from the lantern (0.14+ volumetric fog / 0.18 scattering media). Gate: operator eyes-on — does the swing feel identical, and does the frame read as LANTERN? A fail here kills the fork at spike cost.
  • 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)
Polish is primitives-not-defaults (renderer/anim give capable building blocks; nothing looks good unwired) Medium M1 rig/look proof gate front-loads the judgment; the LANTERN look is shader/atmosphere-driven, which is the favorable case
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)

  1. Sim logic lives in FixedUpdate only; presentation in Update, observe-only.
  2. No wall clock, no unseeded RNG, no HashMap-order dependence anywhere in lantern_core or lantern_sim; the determinism harness is the enforcement.
  3. Derive, don't replicate — if it's a function of replicated state, compute it on both sides.
  4. Ticks are wrapping; compare only through tickmath; zero/default stored tick = not-ready, never fire.
  5. No Bevy Events in rolled-back systems; sim events are components/queues.
  6. All hits swept; every new hit path ships with a tunnelling test.
  7. One-shot actions gate on press edges; every ability ships with a one-press-one-action netcode test.
  8. Every knob: default in lantern_core, override in a RON profile; profiles are experiments, winners fold into defaults.
  9. lantern_core never imports bevy/lightyear/avian — enforce with a CI dep-check.
  10. Version-pin the matrix; engine/netcode upgrades are scheduled tasks with migration-guide reads, never drive-by bumps.
  11. Generator changes bump GenParams version; canonical seeds are snapshot-pinned.
  12. 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).