@waica/engine
v0.6.1
Published
Waica game engine core — archetype-driven, web-first, 2D & 3D
Downloads
1,143
Readme
@waica/engine
Waica's public engine core: entities and components, the game loop, scene and prefab loading, state machines, input, collisions, sprites, camera, stats, and UI.
import { Component, Game, loadScene } from '@waica/engine'Component lifecycle
Waica keeps the lifecycle boundaries distinct:
Entity.add()mounts a component and calls itsonReadyimmediately. This remains component insertion order so setup behavior does not silently move.- During each simulated frame, entities keep their existing entity order. When an entity's turn begins,
Gamesnapshots and resolves that entity's componentonUpdateschedule, then dispatches only that schedule. - Physical
onContacthooks run fromDynamicBodywhile it updates. HitboxonCollidehooks run after all entity component updates. Their existing component dispatch order is unchanged. Game.onUpdatecallbacks run after component updates, collisions, and camera work; input end-of-frame handling follows them.Entity.destroy()callsonDestroyin component insertion order.
Only classes whose prototype chain implements onUpdate participate in the update schedule. Passive components remain available to their siblings but receive no update position.
Declaring update constraints
An updateable component can declare the sibling writes it must observe with inherited static updateAfter metadata:
import { Component, StateMachine } from '@waica/engine'
export class DamageFlash extends Component {
static override componentName = 'DamageFlash'
static override updateAfter: readonly string[] = ['StateMachine']
override onUpdate(dt: number): void {
const state = this.entity.get(StateMachine)?.current
// This update observes StateMachine's state for the same frame.
void state
void dt
}
}The relation is conditional on co-presence. DamageFlash does not require a StateMachine; when that target is registered but absent from this entity, no edge and no issue are created. A subclass inherits updateAfter when it declares nothing and replaces the inherited list when it declares its own list.
Constraints always win over the tie-break. Whenever several components are ready simultaneously, Waica compares their case-sensitive componentName values in ascending Unicode code-unit order. It never uses locale collation, prefab order, scene order, or editor card grouping. Repeated names in one updateAfter list describe one edge.
Invalid schedules fail closed
Component identity must be unique within an entity, and both sides of a present constraint must implement onUpdate. Waica rejects duplicate component names, unknown targets, passive declarers or present passive targets, self-edges, and multi-component cycles.
At runtime an invalid entity runs no partial update schedule and does not fall back to authored order. Other entities continue updating. The engine logs one diagnostic containing the entity and causes, then logs again only if that entity's composition changes.
Tools can inspect a composition without constructing components:
import { resolveComponentUpdateSchedule, type ComponentClass } from '@waica/engine'
const registry: Record<string, ComponentClass> = { DamageFlash, StateMachine }
const result = resolveComponentUpdateSchedule(
['DamageFlash', 'StateMachine'],
registry,
)
if (result.ok) {
console.log(result.order) // ['StateMachine', 'DamageFlash']
} else {
console.error(result.issues)
}The resolver is pure. Pass the effective component-name list and the complete class registry, including project-owned classes. A valid result contains order and no issues; an invalid result contains typed, actionable issues and no executable order.
Runtime inspection
The engine owns Runtime Bridge protocol 1, but it is dormant during ordinary execution: there is no string-named global, network endpoint or per-frame bridge work. An MCP-owned browser context can install the symbol-keyed ephemeral activation hook before navigation. In that context Game.start() registers the fully constructed Game at a paused frame-zero baseline; Game.dispose() or page unload unregisters it.
Runtime Snapshots automatically inspect public own component fields and setter-backed accessors while excluding _ fields, entity, game and functions. A component can replace automatic discovery with the optional public contract:
class PathFinder extends Component {
inspectState(): unknown {
return { target: this.target, remaining: this.path.length }
}
}The return value still passes through the bounded safe projector; it is not serialized with arbitrary toJSON(). The package root exports the Runtime Snapshot, projection marker, metadata, control and activation types plus RUNTIME_BRIDGE_PROTOCOL_VERSION and RUNTIME_PROJECTION_LIMITS.
