Shards of Stone is architected around a strict boundary between a deterministic, lockstep simulation (running at a fixed 20 ticks per second) and an asynchronous presentation layer (rendering at 60+ frames per second via Three.js and HTML5 Canvas).
Third-party developers, custom map authors, and modders can hook into this architecture at three distinct levels:
- TriggerScript AST: Authored inside The Forge map editor or compiled from JSON without writing code.
- Native EventBus Listeners: Subscribing to canonical gameplay, combat, spell, economy, and victory events.
- Custom ECS Systems: Registering custom
Systemclasses directly into the simulation'sWorldloop.
This guide details the decoupled event architecture, provides the complete catalog of 30+ canonical event hooks with exact TypeScript payload schemas, and outlines the determinism rules required to prevent multiplayer desyncs.
Architecture: The Decoupled Event Pipeline
The engine separates simulation authority from presentation side-effects. In standard browser play, the simulation can run on a background Web Worker (SimWorker.ts) or synchronously in single-player tests. The main thread hosts the React UI, HUD components, audio playback (SoundManager), and 2D/3D renderers (World3DRenderer, Model3DRenderer, CombatVFXRenderer).
flowchart TB
subgraph Sim["Deterministic Simulation Authority (20 Ticks/Sec)"]
TickLoop["GameLoop (dt = 0.05s)"]
World["ECS World"]
SimSystems["Systems (Combat, Spells, Build, AI, Triggers)"]
SimBus["Simulation EventBus"]
TickLoop --> World
World --> SimSystems
SimSystems -->|emit| SimBus
end
subgraph Bridge["Cross-Boundary EventBridge"]
SimBus -->|Whitelisted sim->main| EB["EventBridge (__bridged: true)"]
EB -->|Whitelisted main->sim| SimBus
end
subgraph Main["Main Presentation Thread (60+ FPS Interpolated)"]
MainBus["Main Thread EventBus"]
Renderer["3D/2D Renderers (Three.js / Canvas)"]
Audio["SoundManager / VoiceLineSystem"]
HUD["React HUD / Victory Banners"]
UserInput["Player Input (Spell targeting, Build clicks)"]
EB -->|Re-emit| MainBus
MainBus --> Renderer
MainBus --> Audio
MainBus --> HUD
UserInput -->|emit| MainBus
MainBus -->|User Intent| EB
endThe Per-Match EventBus (src/game/core/EventBus.ts)
Every match instantiates a dedicated EventBus. The engine purposefully avoids a persistent global singleton:
- Memory Leak Defense: When a match concludes,
game.destroy()invokeseventBus.clear(). This nulls the internal listener map, severing any closures that forgot to call.off(). - Live Binding:
replaceActiveEventBus()installs a freshly allocated bus at the start of every match. The module-levelexport let eventBusupdates in-place, allowing ES module consumers to retain live bindings. - Directional Isolation (
EventBridge.ts): In Web Worker mode, both the worker and the main thread maintain separateEventBusinstances. TheEventBridgeroutes only whitelisted messages acrosspostMessage(), tagging each serialized payload with__bridged: trueto eliminate infinite echo loops.
import { eventBus } from '@/game/core/EventBus';
// Registering a listener
function onMeleeHit(data: any) {
console.log('Melee strike landed:', data);
}
eventBus.on('melee-attack-hit', onMeleeHit);
// Clean up when dismantling your module or system
eventBus.off('melee-attack-hit', onMeleeHit);Canonical Event Catalog
Below is the exhaustive catalog of canonical engine hooks. All payload interfaces are guaranteed by the engine's serialization contracts and are safe for structured cloning across thread boundaries.
1. Simulation Lifecycle Hooks
Lifecycle events govern match initialization, entity birth/destruction, construction milestones, and victory declaration.
| Event Name | Direction | Primary Consumers | Description |
|---|---|---|---|
mapInit | Sim internal | TriggerSystem | Evaluates map-initialization variables and tick-0 scenario setup. |
onMatchStart | Sim $\rightarrow$ Main | HUD, Camera, Music | Fired when map placement finishes and players gain control. |
entity-died | Sim $\rightarrow$ Main | DeathSystem, Ragdoll, VFX | Emitted whenever any unit, building, or creep dies. |
unit-trained | Sim $\rightarrow$ Main | ProductionSystem, Audio, HUD | Emitted when a training queue item completes and spawns. |
building-complete | Sim $\rightarrow$ Main | BuildSystem, Minimap, Audio | Fired when a building site reaches 100% build progress. |
building-upgraded | Sim $\rightarrow$ Main | BuildingUpgradeSystem, HUD | Fired when a building finishes upgrading to a higher tier. |
upgrade-complete | Sim $\rightarrow$ Main | ResearchSystem, Tech Tree | Fired when a blacksmith or library finishes researching a tech. |
game-over | Sim $\rightarrow$ Main | Victory Screen, Result Ledger | Concludes the match with final winner and end state. |
entity-died Payload Schema (DeathPayload)
The death payload carries complete entity metadata so visual renderers and ragdoll managers never need to call world.getEntity(id) across thread boundaries:
export interface DeathPayload {
readonly entityId: number;
/** Owning player index (0..7), or -1 for neutral critters. */
readonly playerId: number;
readonly isBuilding: boolean;
readonly isFlying: boolean;
readonly isNaval: boolean;
/** True for defensive walls and palisades. */
readonly isWall: boolean;
/** True for destructible creep camps and spawn nests. */
readonly isCreepSpawner: boolean;
readonly pixelX?: number;
readonly pixelY?: number;
readonly gridX?: number;
readonly gridY?: number;
/** Footprint dimensions in tiles (buildings only). */
readonly gridW?: number;
readonly gridH?: number;
readonly unitTypeId?: string;
readonly buildingTypeId?: string;
readonly buildingRole?: string;
readonly buildProgress?: number;
readonly buildingComplete?: boolean;
readonly wallTier?: 1 | 2 | 3;
/** Entity ID of the killer; omitted for decay or scripted suicide. */
readonly killerEntityId?: number;
/** 8-way directional heading (0..7) at the exact moment of death. */
readonly direction?: Direction;
}Other Lifecycle Payload Schemas
// 'unit-trained'
export interface UnitTrainedPayload {
readonly unitType: string;
readonly playerId: number;
readonly entityId?: number;
}
// 'building-complete'
export interface BuildingCompletePayload {
readonly entityId: number;
readonly buildingType: string;
readonly playerId: number;
}
// 'building-upgraded'
export interface BuildingUpgradedPayload {
readonly entityId: number;
readonly oldType: string;
readonly newType: string;
readonly playerId: number;
}
// 'upgrade-complete'
export interface UpgradeCompletePayload {
readonly upgradeId: string;
readonly playerId: number;
}
// 'game-over'
export interface GameOverPayload {
readonly winner: number; // Winning team or player ID (-1 for draw)
readonly state: GameState; // GameState.VICTORY or GameState.DEFEAT
}2. Combat & Damage Hooks
Combat events trigger unit hit reactions, projectile paths, area-of-effect rings, and screen trauma.
| Event Name | Direction | Description |
|---|---|---|
attack-start | Sim $\rightarrow$ Main | Authoritative windup start; contains synchronized animation cues. |
melee-attack-hit | Sim $\rightarrow$ Main | Authoritative melee damage application and impact coordinates. |
projectile-resolved | Sim $\rightarrow$ Main | Projectile hits target or fizzles at ground point. |
aoe-impact | Sim $\rightarrow$ Main | Area damage/heal detonation ring; carries spell ID and chain bounces. |
cone-blast-vfx | Sim $\rightarrow$ Main | Directional spray/blast muzzle (Steam Tank shot, dragon breath). |
crit-landed | Sim $\rightarrow$ Main | Critical hit event; triggers floating gold text and sparkle particles. |
entity-attacked | Sim $\rightarrow$ Sim/Main | High-level damage notification driving worker retargeting and alarms. |
Combat Payload Schemas
import type { DamageType } from '@/game/data/constants';
import type { AnimationCueDef } from '@/game/data/animationCues';
// 'attack-start'
export interface AttackStartEvent {
readonly attackerEntityId: number;
readonly targetEntityId: number;
readonly attackKind: 'melee' | 'ranged';
readonly unitTypeId?: string;
readonly projectileType?: string;
readonly sourcePixelX: number;
readonly sourcePixelY: number;
readonly targetPixelX: number;
readonly targetPixelY: number;
readonly cues: readonly AnimationCueDef[];
}
// 'melee-attack-hit'
export interface MeleeAttackHitPayload {
readonly attackerEntityId: number;
readonly targetEntityId: number;
readonly attackerPixelX: number;
readonly attackerPixelY: number;
readonly targetPixelX: number;
readonly targetPixelY: number;
readonly damage: number;
readonly unitTypeId?: string;
readonly direction: number;
readonly damageType: DamageType;
readonly bonusElement: string | null;
readonly isCrit: boolean;
}
// 'projectile-resolved'
export interface ProjectileResolvedPayload {
readonly targetId: number;
readonly damage: number;
readonly pixelX: number;
readonly pixelY: number;
readonly projectileType: string;
readonly damageType: DamageType;
readonly bonusElement: string | null;
readonly isCrit: boolean;
readonly aoeRadius?: number;
readonly sourceUnitTypeId?: string;
}
// 'aoe-impact'
export interface AoeImpactPayload {
readonly pixelX: number;
readonly pixelY: number;
readonly radius: number;
readonly color: string;
readonly spellId?: string;
/** Ordered list of entity targets for multi-hop chain abilities. */
readonly chainHops?: Array<{ entityId: number; x: number; y: number }>;
}
// 'cone-blast-vfx'
export interface ConeBlastPayload {
readonly entityId: number;
readonly sourcePixelX: number;
readonly sourcePixelY: number;
readonly angle: number; // Radians
readonly halfAngle: number; // Radians
readonly lengthPx: number;
readonly color: string;
}
// 'crit-landed'
export interface CritLandedPayload {
readonly attackerEntityId: number;
readonly targetEntityId: number;
readonly pixelX: number;
readonly pixelY: number;
readonly damage: number;
readonly isMelee: boolean;
}
// 'entity-attacked'
export interface EntityAttackedPayload {
readonly targetEntityId: number;
readonly attackerEntityId: number;
readonly targetPlayerId: number;
readonly attackerPlayerId: number;
readonly damage?: number;
}3. Spells & Abilities Hooks
Spell events handle cast bars, channeling, invocation failures, and special visual effects.
| Event Name | Direction | Description |
|---|---|---|
cast-start | Sim $\rightarrow$ Main | Spell channeling began; drives unit cast bars and windup glows. |
cast-complete | Sim $\rightarrow$ Main | Channeling completed successfully; spell effects execute. |
cast-cancel | Sim $\rightarrow$ Main | Channeling interrupted by stun, silence, movement, or death. |
spell-cast | Sim $\rightarrow$ Main | Triggered when active spell effects resolve into the world. |
heal-vfx | Sim $\rightarrow$ Main | Green restorative sparkle burst over healed target. |
polymorph-vfx | Sim $\rightarrow$ Main | Smoke poof and silhouette transformation into a critter. |
raise-dead-vfx | Sim $\rightarrow$ Main | Necromantic summoning ring emerging from consumed corpses. |
summon-vfx | Sim $\rightarrow$ Main | Portal or glyph flash when conjuring extra units. |
spell-failed | Sim $\rightarrow$ Main | Client feedback toast when cast is denied (mana, cooldown, range). |
Spells Payload Schemas
// 'cast-start'
export interface CastStartEvent {
readonly entityId: number;
readonly spellId: string;
readonly totalTicks: number;
readonly castAnimSlot: 1 | 2 | 3;
readonly sourcePixelX: number;
readonly sourcePixelY: number;
readonly targetEntityId?: number;
readonly targetPixelX?: number;
readonly targetPixelY?: number;
readonly targetGridX?: number;
readonly targetGridY?: number;
readonly cues: readonly AnimationCueDef[];
}
// 'cast-complete' & 'cast-cancel'
export interface CastCompletePayload {
readonly entityId: number;
readonly spellId: string;
}
export interface CastCancelPayload {
readonly entityId: number;
readonly spellId: string;
readonly reason: 'stunned' | 'moved' | 'died' | 'silenced' | 'manual';
}
// 'spell-failed'
export interface SpellFailedPayload {
readonly reason: 'not_enough_mana' | 'cooldown' | 'out_of_range' | 'invalid_target';
readonly playerId: number;
readonly casterEntityId: number;
}
// 'heal-vfx'
export interface HealVfxPayload {
readonly targetEntityId: number;
readonly amount: number;
readonly pixelX: number;
readonly pixelY: number;
}
// 'polymorph-vfx'
export interface PolymorphVfxPayload {
readonly pixelX: number;
readonly pixelY: number;
readonly color: string;
readonly spellId: string;
}
// 'raise-dead-vfx'
export interface RaiseDeadVfxPayload {
readonly pixelX: number;
readonly pixelY: number;
readonly corpseCount: number;
}
// 'summon-vfx'
export interface SummonVfxPayload {
readonly pixelX: number;
readonly pixelY: number;
readonly unitTypeId: string;
}4. Items & Economy Hooks
Hooks associated with hero inventory, shops, resource drop-offs, and creep rewards.
| Event Name | Direction | Description |
|---|---|---|
item-purchased | Sim $\rightarrow$ Main | Item bought from a neutral or base shop. |
item-equipped | Sim $\rightarrow$ Main | Item shifted into an active equipment socket. |
item-used | Sim $\rightarrow$ Main | Active consumable or artifact activated from inventory. |
item-dropped | Sim $\rightarrow$ Main | Item dropped onto terrain or floating on water. |
item-sold | Sim $\rightarrow$ Main | Item sold back to shopkeeper for gold refund. |
resource-gathered | Sim $\rightarrow$ Main | Worker delivers gold, lumber, stone, or oil to a drop site. |
creep-bounty | Sim $\rightarrow$ Main | Neutral creep slain; awards gold and triggers loot rolls. |
Economy Payload Schemas
// 'item-purchased'
export interface ItemPurchasedPayload {
readonly playerId: number;
readonly shopEntityId: number;
readonly heroEntityId: number;
readonly itemId: string;
readonly cost: number;
}
// 'item-equipped'
export interface ItemEquippedPayload {
readonly heroEntityId: number;
readonly itemId: string;
readonly slotIndex: number;
readonly statsDelta?: Record<string, number>;
}
// 'item-used'
export interface ItemUsedPayload {
readonly heroEntityId: number;
readonly playerId: number;
readonly itemId: string;
readonly slotIndex: number;
}
// 'item-dropped'
export interface ItemDroppedPayload {
readonly heroEntityId: number;
readonly itemId: string;
readonly gridX: number;
readonly gridY: number;
readonly groundEntityId: number;
}
// 'item-sold'
export interface ItemSoldPayload {
readonly heroEntityId: number;
readonly playerId: number;
readonly itemId: string;
readonly goldRefund: number;
}
// 'resource-gathered'
export interface ResourceGatheredPayload {
readonly resourceType: 'gold' | 'lumber' | 'stone' | 'oil';
readonly amount: number;
readonly playerId: number;
}
// 'creep-bounty'
export interface CreepBountyPayload {
readonly killerPlayerId: number;
readonly gold: number;
readonly entityId: number;
readonly pixelX: number;
readonly pixelY: number;
}5. Victory Conditions & Objectives Hooks
These events drive custom scenario victory criteria, banners, and the spectator HUD. To conserve bridge bandwidth, progress and countdown updates are throttled by the simulation to ~1 Hz (every 20 ticks) rather than broadcasting per tick.
| Event Name | Direction | Cadence | Description |
|---|---|---|---|
wonder-started | Sim $\rightarrow$ Main | Once | Global announcement when Wonder foundation is laid. |
wonder-complete | Sim $\rightarrow$ Main | Once | Wonder construction finishes; starts hold countdown. |
wonder-countdown | Sim $\rightarrow$ Main | ~1 Hz | Remaining ticks on Wonder victory hold timer. |
wonder-progress | Sim $\rightarrow$ Main | ~1 Hz / Cross | Construction progress percentage (quarter milestones). |
king-slain | Sim $\rightarrow$ Main | Once | Regicide king assassinated; triggers empire wipe. |
relic-picked-up | Sim $\rightarrow$ Main | Transition | Hero retrieves a holy relic from ground/water. |
relic-dropped | Sim $\rightarrow$ Main | Transition | Relic dropped when carrier falls or bank collapses. |
control-point-captured | Sim $\rightarrow$ Main | Transition | Monument or hill captured by a contending team. |
score-standings | Sim $\rightarrow$ Main | ~1 Hz | Current score standings under Score Victory rules. |
Victory Payload Schemas
// 'wonder-started' & 'wonder-complete'
export interface WonderStartedPayload {
readonly entityId: number;
readonly playerId: number;
}
export interface WonderCompletePayload {
readonly entityId: number;
readonly playerId: number;
readonly holdTicks: number;
}
// 'wonder-countdown'
export interface WonderCountdownPayload {
readonly entityId: number;
readonly playerId: number;
readonly remainingTicks: number;
}
// 'wonder-progress'
export interface WonderProgressPayload {
readonly entityId: number;
readonly playerId: number;
readonly gridX: number;
readonly gridY: number;
readonly percent: number;
readonly milestone: boolean;
readonly milestonePercent: number;
}
// 'king-slain'
export interface KingSlainEvent {
readonly playerId: number;
readonly teamId: number;
readonly entityId: number;
}
// 'relic-picked-up' & 'relic-dropped'
export interface RelicPickedUpPayload {
readonly relicEntityId: number;
readonly index: number;
readonly heroEntityId: number;
readonly playerId: number;
}
export interface RelicDroppedPayload {
readonly relicEntityId: number;
readonly index: number;
readonly gridX: number;
readonly gridY: number;
readonly playerId: number;
readonly reason: 'carrier_lost' | 'bank_destroyed';
}
// 'control-point-captured'
export interface ControlPointCapturedPayload {
readonly entityId: number;
readonly playerId: number;
readonly teamId: number;
}
// 'score-standings'
export interface ScoreStandingsPayload {
readonly remainingTicks: number;
readonly standings: Array<{
readonly playerId: number;
readonly teamId: number;
readonly score: number;
}>;
}How 3rd-Party Developers Hook In
Developers have three distinct options depending on the scope of their extension.
Method 1: In-Game Scripting via TriggerScript AST
For custom maps, scenarios, and mods distributed via map files, modders use the Trigger AST. Triggers are authored as pure JSON nodes that compile into the map without executing raw JavaScript.
Triggers hook into simulation events using the events array:
{
"id": "trig_boss_phase_2",
"name": "Boss Enrage at 50% HP",
"enabled": true,
"events": [
{
"k": "unitAttacked",
"unit": { "k": "entityByAid", "aid": "boss_dragon" }
}
],
"conditions": [
{
"k": "lt",
"a": {
"k": "unitProp",
"unit": { "k": "entityByAid", "aid": "boss_dragon" },
"prop": "hp"
},
"b": 2500
},
{
"k": "eq",
"a": { "k": "var", "name": "phase2_triggered" },
"b": false
}
],
"actions": [
{ "k": "setVar", "name": "phase2_triggered", "val": true },
{
"k": "raiseEvent",
"name": "boss-enraged",
"data": { "bossAid": "boss_dragon" }
},
{
"k": "order",
"order": "attack_move",
"units": { "k": "unitsWithTag", "tag": "minions" },
"target": { "k": "regionCenter", "region": "player_base" }
}
]
}Key Trigger Actions for Inter-Trigger Communication:
raiseEvent: Fires a custom named event that other triggers can listen for via{ "k": "customEvent", "name": "..." }.runTrigger: Directly invokes another trigger, bypassing its event and condition guards.enableTrigger/stopTrigger: Toggles trigger activation state dynamically at runtime.
Method 2: Native JavaScript/TypeScript Extension via EventBus
When developing engine plugins, external tools, web spectator overlays, or custom game modes bundled with the engine, you can attach listeners directly to eventBus:
import { eventBus } from '@/game/core/EventBus';
import type { DeathPayload } from '@/game/systems/deathPayload';
import type { AttackStartEvent } from '@/game/data/animationCues';
export class CombatTelemetryLogger {
private boundOnDeath = (data: unknown) => this.onEntityDied(data as DeathPayload);
private boundOnAttack = (data: unknown) => this.onAttackStart(data as AttackStartEvent);
init(): void {
eventBus.on('entity-died', this.boundOnDeath);
eventBus.on('attack-start', this.boundOnAttack);
}
destroy(): void {
eventBus.off('entity-died', this.boundOnDeath);
eventBus.off('attack-start', this.boundOnAttack);
}
private onEntityDied(payload: DeathPayload): void {
if (payload.isBuilding) {
console.log(`Building ${payload.buildingTypeId} razed for player ${payload.playerId}`);
}
}
private onAttackStart(event: AttackStartEvent): void {
if (event.attackKind === 'ranged') {
console.log(`Ranged attack from ${event.attackerEntityId} to ${event.targetEntityId}`);
}
}
}[!WARNING] Always unregister listeners in
destroy(). WhileEventBus.clear()flushes listeners when a match ends, long-running single-player dev sessions or hot-reloading will leak closures if unsubscriptions are skipped.
Method 3: Custom ECS Systems (../ecs/System)
For logic that must run continuously inside the simulation tick loop, developers subclass System and register it with world.addSystem().
Step 1: Subclass System
import { System } from '@/game/ecs/System';
import { eventBus } from '@/game/core/EventBus';
import type { Position } from '@/game/components/Position';
import type { Health } from '@/game/components/Health';
export class PoisonSwampSystem extends System {
// Systems with lower priority numbers update earlier in the tick.
// 50 runs after MovementSystem (20) but before CombatSystem (60).
priority = 50;
private tickAccumulator = 0;
init(): void {
// Systems can register event listeners during initialization
console.log('[PoisonSwampSystem] Initialized in World');
}
update(dt: number): void {
this.tickAccumulator++;
// Run environmental check every 20 ticks (1 second)
if (this.tickAccumulator % 20 !== 0) return;
// DETERMINISM RULE: Always query entities using canonically sorted queries!
const entities = this.world.getEntitiesWith('position', 'health', 'owner');
for (const entity of entities) {
// Never affect dead or flying entities
if (entity.hasComponent('flying')) continue;
const pos = entity.getComponent<Position>('position');
const health = entity.getComponent<Health>('health');
// Check if entity is standing on swamp terrain (assumes custom tile check)
if (this.isSwampTile(pos.gridX, pos.gridY)) {
const damage = 5;
health.current = Math.max(1, health.current - damage);
// Notify presentation layer of environmental damage tick
eventBus.emit('combat-number', {
entityId: entity.id,
amount: damage,
kind: 'poison',
pixelX: pos.pixelX,
pixelY: pos.pixelY,
});
}
}
}
private isSwampTile(x: number, y: number): boolean {
// Custom terrain inspection logic...
return false;
}
destroy(): void {
// Clean up timers or internal buffers
}
}Step 2: Register with the ECS World
import { world } from '@/game/ecs/WorldInstance';
import { PoisonSwampSystem } from './PoisonSwampSystem';
// Add the system with its defined priority
const swampSystem = new PoisonSwampSystem();
world.addSystem(swampSystem, swampSystem.priority);Strict Determinism Rules for Sim-Side Callbacks
In a lockstep peer-to-peer or worker-authoritative architecture, every client must produce bit-for-bit identical state on every tick. Any divergence causes an immediate, unrecoverable desync.
Third-party developers writing simulation logic or listening to sim events MUST adhere to the following four rules:
1. Strictly Synchronous Execution
Sim-side callbacks and system updates must execute synchronously within the tick.
- ❌ Never use
async/await,Promise,setTimeout(),setInterval(), orrequestAnimationFrame()inside simulation logic. - ❌ Never defer simulation state changes to the next microtask.
- If an action takes time (such as a 3-second spell cast or a build queue), store a
remainingTicks: numbercounter in an ECS component and decrement it synchronously each tick.
2. Bit-Exact & Deterministic Math
Different JavaScript engines (Google V8, Mozilla SpiderMonkey, Apple JavaScriptCore) implement transcendental math functions slightly differently.
- ❌ Banned:
Math.random(). Instead, usegameRandom.nextInt(min, max)or seed-based deterministic hashing (fnv1a). - ❌ Banned:
Date.now()andperformance.now(). The simulation's only clock is the integer tick count (world.ticks). - ❌ Banned:
Math.sin(),Math.cos(),Math.tan(),Math.hypot(). Use integer lookup tables or bit-exact integer square roots (detHypot). - All floating-point operations should be bounded and truncated with
Math.trunc()or rounded via integer math (Math.floor(x + 0.5)).
3. Canonical Entity Iteration Order
JavaScript Set and object key iteration order is insertion-order dependent. If Client A spawned a unit before a building, and Client B loaded the map in reverse order, iterating a raw Set will evaluate actions in different orders, causing desyncs when spatial budgets or targeting slots are contested.
// ❌ WRONG: Non-deterministic iteration order
const set = world.getComponentIndex('combat');
for (const entityId of set) {
// Diverges across clients!
}
// ❌ WRONG: Using unsorted render queries in simulation
const entities = world.getEntitiesWithIntoUnsortedRenderOnly([], 'combat', 'position');
// ✅ CORRECT: getEntitiesWith is canonically sorted by entity.id (ascending)
const entities = world.getEntitiesWith('combat', 'position');
// ✅ CORRECT: Allocation-free sorted query for hot tick loops
const scratchBuffer: Entity[] = [];
world.getEntitiesWithIntoSorted(scratchBuffer, 'combat', 'position');
for (const entity of scratchBuffer) {
// 100% deterministic on all machines!
}4. Snapshot Independence in Loops
When modifying entities inside a loop, take a snapshot of the candidate list before mutating components or destroying entities:
// ✅ CORRECT: Snapshot array prevents mid-loop index corruption
const targets = world.getEntitiesWith('health');
for (const entity of targets) {
if (shouldDie(entity)) {
entity.destroy(); // Safe because 'targets' is a stable snapshot
}
}Previous: Custom Units & 3D Meshy Models. Next: Web APIs & Headless SDK.