Spec the future Bevy WebAssembly viewer layer #16
Labels
No labels
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/factory-game-v3#16
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Write a design/spec for a future Bevy viewer layer for
factory-game-v3. This is a spec task only. Do not implement the viewer yet.Current architecture context:
factory_simshould stay pure deterministic Rust and platform-neutral.factory_contentowns starter content/scenario data.factory_cliis the current headless observability surface.factory-game-v3#15added typed dispatch protocol state into the Rust simulation.Design goal:
Specify a future
factory_viewercrate using Bevy that can eventually target desktop and browser/WebAssembly without dragging Bevy or web constraints intofactory_sim.Questions the spec should answer:
factory_simsnapshots/events without owning simulation rules?wasm32-unknown-unknown, or both?trunkunless a better Bevy convention exists.Constraints:
factory_sim.Expected output:
Out of scope:
factory_simunless the spec identifies a future boundary issue.Close this when the Bevy/Wasm viewer spec is posted as a comment.
🔎 ward agent advisor
ward agent advisor --harness codexran a one-shot standard research pass on this question:Recommendation
Add a single new workspace crate,
crates/factory_viewer, as a debug/observability viewer that depends onfactory_simandfactory_content, but keep all simulation authority infactory_sim.The repo already has the right core split for this:
factory_simowns deterministic stepping and emits serializableTickSnapshots,factory_contentowns starter data, andfactory_cliis the current headless surface. The viewer should sit beside them as a projection layer, not as a rules layer.As of July 9, 2026, Bevy's official web examples still emphasize WASM + WebGL for broad browser compatibility, while WebGPU is presented separately. That makes a 2D, WebGL2-compatible, observability-first viewer the safest first slice for both native and browser targets. Trunk is still the cleanest packaging path for a small Rust/WASM app.
Recommended crate and workspace shape
Keep the workspace shape simple first:
Recommended dependency direction:
factory_viewer -> factory_simfactory_viewer -> factory_contentfactory_cli -> factory_simfactory_simstays Bevy-freeDo not add a separate
factory_viewer_webcrate yet. One crate withcfg(target_arch = "wasm32")platform shims is enough until there is real divergence.Boundary between sim and Bevy
The viewer should consume immutable snapshots and events, not simulation internals as mutable ECS state.
Recommended rule:
factory_simownsGameState, deterministic tick order, dispatch rules, inventories, production rules, and authoritative IDs/state.factory_viewerowns camera, scene graph, labels, colors, controls, interpolation policy, and UI state.TickSnapshot, not the source of truth.For the first implementation, define a viewer-local driver abstraction in
factory_viewer, not infactory_simyet. For example:Initial adapter:
SimSnapshotSourcewrapsfactory_sim::GameStateFuture adapters, without changing the viewer architecture:
factory_clioutputThat gives the viewer a clean seam without prematurely changing
factory_sim.Boundary between deterministic ticks and Bevy frame scheduling
Use two clocks:
Recommended behavior:
PlaybackState { paused, ticks_per_second, accumulated_time }.TickSnapshotto the Bevy projection.Important guardrails:
max_ticks_per_frame, to avoid a spiral on slow browsers.pause,resume, andsingle-stepfrom day one.For the first slice, do not interpolate gameplay state between ticks. Stepwise movement is acceptable for a debug viewer and preserves determinism clarity.
Minimum first viewer slice worth building
The smallest slice that justifies the crate is a 2D orthographic debug viewer for the existing
iron-barsscenario.Include:
play/pause,single-step,reset, and at least one faster speed like4xTickSnapshot.eventsPresentation can stay intentionally simple:
This is enough to validate the architecture without pretending to be the future game client.
Build targets
Recommendation: design for both native and wasm immediately, but make native the primary dev loop.
That means:
Why this split is the right trade:
For the web renderer path, prefer WebGL2-compatible Bevy usage first, not WebGPU-only features. Bevy's official examples page still treats WebGPU as separate while the main browser examples run on WASM + WebGL.
Web packaging path
Prefer Trunk.
Reasoning:
copy-dir/copy-filemodel fits a small viewer assets folder well.Suggested future commands for the implementation issue:
Add repo verbs when the viewer lands, likely wrapping commands equivalent to:
and for a production wasm build:
Note: Bevy's current migration guidance for web builds calls out the
getrandom/RUSTFLAGSrequirement for wasm targets, so the implementation issue should account for that explicitly.Assets strategy for wasm
For the first viewer slice, use viewer-owned local static assets or generated placeholders, not Unity-era asset reuse.
Recommended priority:
Why:
.metanoise, and future LFS decisions.If art reuse happens later, treat it as a deliberate import/export task into a viewer-owned asset directory, not direct runtime dependence on the existing Unity
Assets/tree.Feature gates and platform boundaries
Use
cfg(target_arch = "wasm32")for platform differences first. Do not over-engineer cargo feature matrices yet.Platform-specific behavior that should be isolated behind thin shims:
Keep shared viewer logic platform-neutral:
TickSnapshotAvoid in milestone 1:
Out of scope until after the first slice
Keep these explicitly out of milestone 1:
factory_clifactory_simRisks and open questions
Risks
TickSnapshotmay become too coarse or too allocation-heavy. That is a future optimization problem, not a blocker for the first slice.Open questions worth deferring, not blocking on
factory_clioutput or from a shared recorded formatVec<String>None of those should block the first implementation issue.
Proposed acceptance criteria for a later implementation issue
crates/factory_viewerto the workspace without adding Bevy tofactory_sim.factory_viewerruns the existing starteriron-barsscenario by wrappingfactory_sim::GameState.pause,resume,single-step, andreset.wasm32-unknown-unknownbuild works locally and can be served through Trunk.factory_simremains platform-neutral and Bevy-free.Source notes
getrandom/RUSTFLAGSrequirements: https://bevy.org/learn/migration-guides/0-16-to-0-17/copy-dir/copy-file: https://trunk-rs.github.io/trunk/guide/assets/index.htmlResearched and posted automatically by
ward agent advisor --harness codex(ward#179). This is one-shot research, not a carried change - verify before acting on it.— Codex, via
ward agentClarification from Kai after the advisor comment: do not mix galaxy generation into
factory-game-v3.This issue should be read only as a future factory-game Bevy/Wasm debug viewer over
factory_sim. Galaxy generation and any Bevy galaxy viewer are a separate app/repo concern, not afactory_gamecrate or milestone.Bevy/Wasm viewer spec, informed by the landed #18 shell (
crates/factory_shell,7cd29b4) and bounded by docs/app-boundary.md.Crate/workspace shape
crates/factory_viewer, sibling tofactory_shell. Depends onfactory_sim+factory_content(read-only consumption) and Bevy.factory_simstays pure and Bevy-free - the viewer is a client of its public API, never a host for its rules.factory_shellstays the packaging exemplar (trunk from the crate dir, nginx Dockerfile). Recommended shape: the viewer replaces the shell's placeholder scene behind the same packaging, keeping one deployable app - the shell crate's scene was always scaffolding.Bevy <-> sim boundary
GameStateinside a Bevy resource (for exampleSimHost { state: GameState, cadence: Timer }).WorldStatedirectly. One system callsstate.step()on a fixed cadence (default 2 ticks/sec, pause + single-step controls). Everything rendered derives from the returnedTickSnapshot, never from reaching intoGameStateinternals.position_gridis later polish.Serializeand stable - the viewer consumes the same shape the CLI emits, which keeps a future load-a-JSONL-recording playback mode trivial and requires no live sim.Minimum first slice
TopologySnapshotgrid positions: sources, road, factory as colored quads with labels.position_gridwith cargo count and assignment phase as text.iron-bars) in slice one.Build targets and packaging
wasm32-unknown-unknownfrom day one - the shell proves both paths and the viewer rides the same trunk + Dockerfile pipeline.agentic-os:v0.255.0carries the full toolchain in CI.shell-*verbs serve whichever scene ships) or gainviewer-*twins - implementer's choice.Assets
Out of scope until after slice one
Interpolated movement, camera controls beyond a fixed 2D view, scenario editing, live parameter tweaking, in-app scenario switching, recording/replay UI (the JSONL playback hook stays a design affordance), and anything galaxy-shaped.
Acceptance criteria for the implementation issue
cargo test --workspacepasses, including one headless test provingSimHoststepping produces byte-identical snapshots to callingGameState::step()directly - determinism preserved under the Bevy host.factory_sim/factory_contentgain no Bevy dependency.Risks
factory_shellpins at the time (0.19 today).WARD-OUTCOME: done