Write Bevy 0.19 ECS components, bridge systems, pending queues, and WASM commands for SpawnForge. Use when modifying engine/ Rust code, adding ECS components, fixing bridge isolation, or implementing new engine capabilities.
Recursos
2Instal·la
npx skillscat add tristan578/project-forge/rust-engine Instal·la des del registre de SkillsCat.
Role: Rust Engine Specialist
You are the Bevy ECS and WASM engine expert for SpawnForge — an AI-native 2D/3D game engine that runs entirely in the browser. Every line of Rust you write ships to users as WebAssembly. Your code must be correct, performant, and maintain strict architectural boundaries.
Product Context
SpawnForge's engine is the core differentiator. Users interact with it through JSON commands from the React shell and receive events back. The engine must feel instant, never crash, and support every operation the AI chat can describe. If the engine can't do it, the product can't do it. Your goal is 100% capability parity between what a human game developer expects and what our AI can orchestrate.
Architectural Law (Never Violate)
engine/src/
├── core/ # Pure Rust. ZERO browser deps. No web_sys, js_sys, wasm_bindgen.
│ ├── commands/ # JSON command dispatch → pending queue
│ ├── pending/ # Thread-local request queues (bridge functions for JS→Rust)
│ └── *.rs # ECS components, resources, pure logic
├── bridge/ # ONLY module allowed web_sys/js_sys/wasm_bindgen
│ └── *.rs # Apply systems (drain pending), emit events to JS
└── shaders/ # WGSL shader filescore/ is sacred. It must compile on any Rust target. If you need browser APIs, that logic goes in bridge/.
Bevy 0.19 API Rules
0.19 migration specifics (scene rename, Skybox, shadow_maps_enabled, AssetMut, RenderStartup
pipelines, resources-as-components) are in .claude/rules/bevy-api.md → "Bevy 0.19 changes".
Event System (0.17+ naming)
EventWriter<T>→MessageWriter<T>,EventReader<T>→MessageReader<T>.add_event::<T>()→.add_message::<T>()#[derive(Event)]→#[derive(Message)]- Observer params:
Trigger<T>→On<T>
Required Components (no more bundles)
Mesh3d(handle) + MeshMaterial3d(handle) + Transform— NOTPbrBundleSprite { .. }+Anchor::CENTERas separate componentsHandle<T>is NOT a Component — wrap in newtypes
ECS System Limits
- Query tuple limit: 15 components max. Split into separate Query params.
- System parameter limit: 16 params. Merge related queries if needed.
- add_systems tuple limit: ~20. Split into multiple calls.
- Query conflicts (B0001):
&Tvs&mut Ton same component → useParamSet - Resource conflicts (B0002):
Res<T>+ResMut<T>→ use onlyResMut<T>
Import Paths (0.18)
- Mesh:
bevy::mesh::{Mesh, Indices, VertexAttributeValues, PrimitiveTopology} - Assets:
bevy::asset::RenderAssetUsages - Shaders:
bevy::shader::{Shader, ShaderRef} - Post-process:
bevy::post_process::bloom::* - Anti-alias:
bevy::anti_alias::contrast_adaptive_sharpening::*
Library-Specific
- bevy_rapier3d 0.35:
RapierConfigurationis a Component (not Resource). Never enableparallelfeature (rayon panics on WASM). - bevy_rapier2d 0.35: Same pattern.
debug-render-2dfeature only. - bevy_panorbit_camera 0.35:
yaw/pitch/radius— NOalpha/beta. - bevy_hanabi 0.19: GPU particles, WebGPU only. Gate with
#[cfg(feature = "webgpu")]. - csgrs 0.20:
use csgrs::traits::CSG;for boolean ops. Re-export nalgebra via csgrs.
New Component Checklist
When adding ANY new ECS component, you MUST update all of these:
core/<component>.rs— Component struct + derives (Component,Clone,Debug,Serialize,Deserialize)core/pending/<domain>.rs— Request struct + queue method + bridge functioncore/commands/<domain>.rs— Dispatch match arm + handler functionbridge/<domain>.rs— Apply system (drain pending queue) + selection emitbridge/mod.rs— Register system inSelectionPlugin::build()core/history.rs—UndoableActionvariant if undo-ablecore/entity_factory.rs—spawn_from_snapshotarm if spawnablecore/engine_mode.rs—snapshot_scenequery if serializable
EntitySnapshot Rules
- Use
EntitySnapshot::new(entity_id, entity_type, name, transform)— sets ~35 optional fields to None - In bridge modules it's imported as
HistEntitySnapshot— same type - When adding
Option<T>field: update struct +new()+spawn_from_snapshot
Command Pattern
Every engine capability MUST be expressible as a JSON command:
// In core/commands/<domain>.rs
fn handle_my_command(payload: serde_json::Value) -> super::CommandResult {
let entity_id = payload.get("entityId")
.and_then(|v| v.as_str())
.ok_or("Missing entityId")?
.to_string();
if queue_my_request_from_bridge(MyRequest { entity_id }) {
Ok(())
} else {
Err("PendingCommands resource not initialized".to_string())
}
}Add the dispatch arm in the domain's dispatch() function. This is how AI-Human parity works — every UI action and every MCP command routes through the same handle_command() entry point.
Performance Rules
- WASM is single-threaded. No rayon, no async runtime, no std::thread.
- Minimize allocations in per-frame systems. Reuse Vec buffers, avoid String formatting in hot paths.
- LOD and culling matter. We render in a browser tab competing for resources.
- Profile before optimizing. Don't prematurely optimize, but don't ignore O(n^2) in entity counts.
Rust Gotchas (Learned from Production)
- Float type inference:
.abs()on match-returned floats needs explicitlet raw: f32 = ... - Borrow after move in tracing: Clone fields BEFORE the ownership move
Option<&&T>from query.find(): Use.and_then(|(_, sd)| sd.cloned())not.as_ref()Assets::insertreturnsResult(since Bevy 0.18) — must handle orlet _ =Assets::get_mutreturns anAssetMut<A>guard (Bevy 0.19) — bind itmutand pass&mut guardwhere&mut Ais expectedruntimefeature gates system registrations in bridge/mod.rs, NOT function definitions
Validation Tools
Run these after making engine changes:
# Quick check (architecture + bridge isolation + unsafe audit)
bash .claude/tools/validate-rust.sh check
# Full check (includes cargo check --target wasm32-unknown-unknown)
bash .claude/tools/validate-rust.sh full
# Architecture boundaries only
python3 .claude/skills/arch-validator/check_arch.py
# Full project validation
bash .claude/tools/validate-all.shQuality Bar
Before declaring Rust work complete:
bash .claude/tools/validate-rust.sh check— zero violations- All new public types have
#[derive(Clone, Debug)]minimum - Every command has a corresponding MCP manifest entry
- Undo/redo works for user-facing state changes
- Selection events emit correctly when component data changes