@zylem/game-lib 0.6.0 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -16
- package/dist/actions.d.ts +30 -21
- package/dist/actions.js +628 -145
- package/dist/actions.js.map +1 -1
- package/dist/behavior/platformer-3d.d.ts +296 -0
- package/dist/behavior/platformer-3d.js +518 -0
- package/dist/behavior/platformer-3d.js.map +1 -0
- package/dist/behavior/ricochet-2d.d.ts +274 -0
- package/dist/behavior/ricochet-2d.js +394 -0
- package/dist/behavior/ricochet-2d.js.map +1 -0
- package/dist/behavior/screen-wrap.d.ts +86 -0
- package/dist/behavior/screen-wrap.js +195 -0
- package/dist/behavior/screen-wrap.js.map +1 -0
- package/dist/behavior/thruster.d.ts +10 -0
- package/dist/behavior/thruster.js +234 -0
- package/dist/behavior/thruster.js.map +1 -0
- package/dist/behavior/world-boundary-2d.d.ts +141 -0
- package/dist/behavior/world-boundary-2d.js +181 -0
- package/dist/behavior/world-boundary-2d.js.map +1 -0
- package/dist/behavior-descriptor-BWNWmIjv.d.ts +142 -0
- package/dist/{blueprints-BOCc3Wve.d.ts → blueprints-BWGz8fII.d.ts} +2 -2
- package/dist/camera-B5e4c78l.d.ts +468 -0
- package/dist/camera.d.ts +3 -2
- package/dist/camera.js +962 -166
- package/dist/camera.js.map +1 -1
- package/dist/composition-DrzFrbqI.d.ts +218 -0
- package/dist/{core-CZhozNRH.d.ts → core-DAkskq6Y.d.ts} +97 -65
- package/dist/core.d.ts +12 -6
- package/dist/core.js +4449 -1052
- package/dist/core.js.map +1 -1
- package/dist/{entities-BAxfJOkk.d.ts → entities-DC9ce_vx.d.ts} +154 -45
- package/dist/entities.d.ts +5 -2
- package/dist/entities.js +2505 -722
- package/dist/entities.js.map +1 -1
- package/dist/entity-BpbZqg19.d.ts +1100 -0
- package/dist/entity-types-DAu8sGJH.d.ts +26 -0
- package/dist/global-change-Dc8uCKi2.d.ts +25 -0
- package/dist/main.d.ts +472 -29
- package/dist/main.js +11877 -6124
- package/dist/main.js.map +1 -1
- package/dist/{stage-types-CD21XoIU.d.ts → stage-types-BFsm3qsZ.d.ts} +255 -26
- package/dist/stage.d.ts +11 -6
- package/dist/stage.js +3462 -491
- package/dist/stage.js.map +1 -1
- package/dist/thruster-DhRaJnoL.d.ts +172 -0
- package/dist/world-Be5m1XC1.d.ts +31 -0
- package/package.json +21 -4
- package/dist/behaviors.d.ts +0 -106
- package/dist/behaviors.js +0 -398
- package/dist/behaviors.js.map +0 -1
- package/dist/camera-CpbDr4-V.d.ts +0 -116
- package/dist/entity-COvRtFNG.d.ts +0 -395
- package/dist/moveable-B_vyA6cw.d.ts +0 -67
- package/dist/transformable-CUhvyuYO.d.ts +0 -67
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/behaviors/thruster/components.ts","../../src/lib/behaviors/thruster/thruster-fsm.ts","../../src/lib/behaviors/thruster/thruster-movement.behavior.ts","../../src/lib/behaviors/behavior-descriptor.ts","../../src/lib/behaviors/thruster/thruster.descriptor.ts"],"sourcesContent":["/**\n * Thruster-specific ECS Components\n * \n * These components are specific to the thruster movement system.\n */\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ThrusterMovementComponent (capability / hardware spec)\n// Defines the thrust capabilities of an entity\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ThrusterMovementComponent {\n\t/** Linear thrust force in Newtons (or scaled units) */\n\tlinearThrust: number;\n\t/** Angular thrust torque scalar */\n\tangularThrust: number;\n\t/** Optional linear damping override */\n\tlinearDamping?: number;\n\t/** Optional angular damping override */\n\tangularDamping?: number;\n}\n\nexport function createThrusterMovementComponent(\n\tlinearThrust: number,\n\tangularThrust: number,\n\toptions?: { linearDamping?: number; angularDamping?: number }\n): ThrusterMovementComponent {\n\treturn {\n\t\tlinearThrust,\n\t\tangularThrust,\n\t\tlinearDamping: options?.linearDamping,\n\t\tangularDamping: options?.angularDamping,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ThrusterInputComponent (intent)\n// Written by: Player controller, AI controller, FSM adapter\n// Read by: ThrusterMovementBehavior\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ThrusterInputComponent {\n\t/** Forward thrust intent: 0..1 */\n\tthrust: number;\n\t/** Rotation intent: -1..1 */\n\trotate: number;\n}\n\nexport function createThrusterInputComponent(): ThrusterInputComponent {\n\treturn {\n\t\tthrust: 0,\n\t\trotate: 0,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ThrusterStateComponent (optional but useful)\n// Useful for: damage, EMP, overheating, UI feedback\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ThrusterStateComponent {\n\t/** Whether the thruster is enabled */\n\tenabled: boolean;\n\t/** Current thrust after FSM/gating */\n\tcurrentThrust: number;\n}\n\nexport function createThrusterStateComponent(): ThrusterStateComponent {\n\treturn {\n\t\tenabled: true,\n\t\tcurrentThrust: 0,\n\t};\n}\n","/**\n * ThrusterFSM\n * \n * State machine controller for thruster behavior.\n * FSM does NOT touch physics or ThrusterMovementBehavior - it only writes ThrusterInputComponent.\n */\n\nimport { StateMachine, t, type ITransition } from 'typescript-fsm';\nimport type { ThrusterInputComponent } from './components';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM State Model\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport enum ThrusterState {\n\tIdle = 'idle',\n\tActive = 'active',\n\tBoosting = 'boosting',\n\tDisabled = 'disabled',\n\tDocked = 'docked',\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM Events\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport enum ThrusterEvent {\n\tActivate = 'activate',\n\tDeactivate = 'deactivate',\n\tBoost = 'boost',\n\tEndBoost = 'endBoost',\n\tDisable = 'disable',\n\tEnable = 'enable',\n\tDock = 'dock',\n\tUndock = 'undock',\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM Context Object\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ThrusterFSMContext {\n\tinput: ThrusterInputComponent;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Player Input (raw input from controller/keyboard)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface PlayerInput {\n\tthrust: number;\n\trotate: number;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ThrusterFSM Controller\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class ThrusterFSM {\n\tmachine: StateMachine<ThrusterState, ThrusterEvent, never>;\n\n\tconstructor(private ctx: ThrusterFSMContext) {\n\t\tthis.machine = new StateMachine<ThrusterState, ThrusterEvent, never>(\n\t\t\tThrusterState.Idle,\n\t\t\t[\n\t\t\t\t// Core transitions\n\t\t\t\tt(ThrusterState.Idle, ThrusterEvent.Activate, ThrusterState.Active),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Deactivate, ThrusterState.Idle),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Boost, ThrusterState.Boosting),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Disable, ThrusterState.Disabled),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Dock, ThrusterState.Docked),\n\t\t\t\tt(ThrusterState.Boosting, ThrusterEvent.EndBoost, ThrusterState.Active),\n\t\t\t\tt(ThrusterState.Boosting, ThrusterEvent.Disable, ThrusterState.Disabled),\n\t\t\t\tt(ThrusterState.Disabled, ThrusterEvent.Enable, ThrusterState.Idle),\n\t\t\t\tt(ThrusterState.Docked, ThrusterEvent.Undock, ThrusterState.Idle),\n\t\t\t\t// Self-transitions (no-ops for redundant events)\n\t\t\t\tt(ThrusterState.Idle, ThrusterEvent.Deactivate, ThrusterState.Idle),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Activate, ThrusterState.Active),\n\t\t\t]\n\t\t);\n\t}\n\n\t/**\n\t * Get current state\n\t */\n\tgetState(): ThrusterState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Dispatch an event to transition state\n\t */\n\tdispatch(event: ThrusterEvent): void {\n\t\tif (this.machine.can(event)) {\n\t\t\tthis.machine.dispatch(event);\n\t\t}\n\t}\n\n\t/**\n\t * Update FSM state based on player input.\n\t * Auto-transitions between Idle/Active to report current state.\n\t * Does NOT modify input - just observes and reports.\n\t */\n\tupdate(playerInput: PlayerInput): void {\n\t\tconst state = this.machine.getState();\n\t\tconst hasInput = Math.abs(playerInput.thrust) > 0.01 || Math.abs(playerInput.rotate) > 0.01;\n\n\t\t// Auto-transition to report state based on input\n\t\tif (hasInput && state === ThrusterState.Idle) {\n\t\t\tthis.dispatch(ThrusterEvent.Activate);\n\t\t} else if (!hasInput && state === ThrusterState.Active) {\n\t\t\tthis.dispatch(ThrusterEvent.Deactivate);\n\t\t}\n\t}\n}\n","/**\n * ThrusterMovementBehavior\n * \n * This is the heart of the thruster movement system - a pure, stateless force generator.\n * Works identically for player, AI, and replay.\n */\n\nimport type { ZylemWorld } from '../../collision/world';\nimport type { PhysicsBodyComponent } from '../components';\nimport type { ThrusterMovementComponent, ThrusterInputComponent } from './components';\n\n/**\n * Zylem-style Behavior interface\n */\nexport interface Behavior {\n\tupdate(dt: number): void;\n}\n\n/**\n * Entity with thruster components\n */\nexport interface ThrusterEntity {\n\tphysics: PhysicsBodyComponent;\n\tthruster: ThrusterMovementComponent;\n\t$thruster: ThrusterInputComponent;\n}\n\n/**\n * ThrusterMovementBehavior - Force generator for thruster-equipped entities\n * \n * Responsibilities:\n * - Query entities with PhysicsBody, ThrusterMovement, and ThrusterInput components\n * - Apply velocities based on thrust input (2D mode)\n * - Apply angular velocity based on rotation input\n */\nexport class ThrusterMovementBehavior implements Behavior {\n\tconstructor(private world: ZylemWorld) {}\n\n\t/**\n\t * Query function - returns entities with required thruster components\n\t */\n\tprivate queryEntities(): ThrusterEntity[] {\n\t\tconst entities: ThrusterEntity[] = [];\n\t\t\n\t\tfor (const [, entity] of this.world.collisionMap) {\n\t\t\tconst gameEntity = entity as any;\n\t\t\tif (\n\t\t\t\tgameEntity.physics?.body &&\n\t\t\t\tgameEntity.thruster &&\n\t\t\t\tgameEntity.$thruster\n\t\t\t) {\n\t\t\t\tentities.push({\n\t\t\t\t\tphysics: gameEntity.physics,\n\t\t\t\t\tthruster: gameEntity.thruster,\n\t\t\t\t\t$thruster: gameEntity.$thruster,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn entities;\n\t}\n\n\tupdate(_dt: number): void {\n\t\tconst entities = this.queryEntities();\n\n\t\tfor (const e of entities) {\n\t\t\tconst body = e.physics.body;\n\t\t\tconst thruster = e.thruster;\n\t\t\tconst input = e.$thruster;\n\n\t\t\t// Get Z rotation from quaternion (for 2D sprites)\n\t\t\tconst q = body.rotation();\n\t\t\tconst rotationZ = Math.atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z));\n\n\t\t\t// --- Linear thrust (Asteroids-style: adds velocity in forward direction) ---\n\t\t\tif (input.thrust !== 0) {\n\t\t\t\tconst currentVel = body.linvel();\n\t\t\t\t\n\t\t\t\tif (input.thrust > 0) {\n\t\t\t\t\t// Forward thrust: add velocity in facing direction\n\t\t\t\t\tconst forwardX = Math.sin(-rotationZ);\n\t\t\t\t\tconst forwardY = Math.cos(-rotationZ);\n\t\t\t\t\tconst thrustAmount = thruster.linearThrust * input.thrust * 0.1;\n\t\t\t\t\tbody.setLinvel({\n\t\t\t\t\t\tx: currentVel.x + forwardX * thrustAmount,\n\t\t\t\t\t\ty: currentVel.y + forwardY * thrustAmount,\n\t\t\t\t\t\tz: currentVel.z\n\t\t\t\t\t}, true);\n\t\t\t\t} else {\n\t\t\t\t\t// Brake: reduce current velocity (slow down)\n\t\t\t\t\tconst brakeAmount = 0.9; // 10% reduction per frame\n\t\t\t\t\tbody.setLinvel({\n\t\t\t\t\t\tx: currentVel.x * brakeAmount,\n\t\t\t\t\t\ty: currentVel.y * brakeAmount,\n\t\t\t\t\t\tz: currentVel.z\n\t\t\t\t\t}, true);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// --- Angular thrust (Z-axis for 2D) ---\n\t\t\tif (input.rotate !== 0) {\n\t\t\t\tbody.setAngvel({ x: 0, y: 0, z: -thruster.angularThrust * input.rotate }, true);\n\t\t\t} else {\n\t\t\t\t// Stop rotation when no input\n\t\t\t\tconst angVel = body.angvel();\n\t\t\t\tbody.setAngvel({ x: angVel.x, y: angVel.y, z: 0 }, true);\n\t\t\t}\n\t\t}\n\t}\n}\n\n","/**\n * BehaviorDescriptor\n *\n * Type-safe behavior descriptors that provide options inference.\n * Used with entity.use() to declaratively attach behaviors to entities.\n *\n * Each behavior can define its own handle type via `createHandle`,\n * providing behavior-specific methods with full type safety.\n */\n\nimport type { BehaviorSystemFactory } from './behavior-system';\n\n/**\n * Base handle returned by entity.use() for lazy access to behavior runtime.\n * FSM is null until entity is spawned and components are initialized.\n */\nexport interface BaseBehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** Get the FSM instance (null until entity is spawned) */\n getFSM(): any | null;\n /** Get the current options */\n getOptions(): O;\n /** Access the underlying behavior ref */\n readonly ref: BehaviorRef<O>;\n}\n\n/**\n * Reference to a behavior stored on an entity\n */\nexport interface BehaviorRef<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** The behavior descriptor */\n descriptor: BehaviorDescriptor<O, any>;\n /** Merged options (defaults + overrides) */\n options: O;\n /** Optional FSM instance - set lazily when entity is spawned */\n fsm?: any;\n}\n\n/**\n * A typed behavior descriptor that associates a symbol key with:\n * - Default options (providing type inference)\n * - A system factory to create the behavior system\n * - An optional handle factory for behavior-specific methods\n */\nexport interface BehaviorDescriptor<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Unique symbol identifying this behavior */\n readonly key: symbol;\n /** Default options (used for type inference) */\n readonly defaultOptions: O;\n /** Factory to create the behavior system */\n readonly systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * These methods are merged into the handle returned by entity.use().\n */\n readonly createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * The full handle type returned by entity.use().\n * Combines base handle with behavior-specific methods.\n */\nexport type BehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n> = BaseBehaviorHandle<O> & H;\n\n/**\n * Configuration for defining a new behavior\n */\nexport interface DefineBehaviorConfig<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Human-readable name for debugging */\n name: string;\n /** Default options - these define the type */\n defaultOptions: O;\n /** Factory function to create the system */\n systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * The returned object is merged into the handle returned by entity.use().\n *\n * @example\n * ```typescript\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX, moveY) => ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * ```\n */\n createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * Define a typed behavior descriptor.\n *\n * @example\n * ```typescript\n * export const WorldBoundary2DBehavior = defineBehavior({\n * name: 'world-boundary-2d',\n * defaultOptions: { boundaries: { top: 0, bottom: 0, left: 0, right: 0 } },\n * systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX: number, moveY: number) =>\n * ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * });\n *\n * // Usage - handle has getLastHits and getMovement with full types\n * const boundary = ship.use(WorldBoundary2DBehavior, { ... });\n * const hits = boundary.getLastHits(); // Fully typed!\n * ```\n */\nexport function defineBehavior<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n>(\n config: DefineBehaviorConfig<O, H, I>,\n): BehaviorDescriptor<O, H, I> {\n return {\n key: Symbol.for(`zylem:behavior:${config.name}`),\n defaultOptions: config.defaultOptions,\n systemFactory: config.systemFactory,\n createHandle: config.createHandle,\n };\n}\n","/**\n * Thruster Behavior Descriptor\n * \n * Type-safe descriptor for the thruster behavior system using the new entity.use() API.\n * This wraps the existing ThrusterMovementBehavior and components.\n */\n\nimport type { IWorld } from 'bitecs';\nimport { defineBehavior } from '../behavior-descriptor';\nimport type { BehaviorSystem } from '../behavior-system';\nimport { ThrusterMovementBehavior, ThrusterEntity } from './thruster-movement.behavior';\nimport { ThrusterFSM } from './thruster-fsm';\nimport type { ThrusterMovementComponent, ThrusterInputComponent } from './components';\n\n/**\n * Thruster behavior options (typed for entity.use() autocomplete)\n */\nexport interface ThrusterBehaviorOptions {\n\t/** Forward thrust force (default: 10) */\n\tlinearThrust: number;\n\t/** Rotation torque (default: 5) */\n\tangularThrust: number;\n}\n\nconst defaultOptions: ThrusterBehaviorOptions = {\n\tlinearThrust: 10,\n\tangularThrust: 5,\n};\n\n/**\n * Adapter that wraps ThrusterMovementBehavior as a BehaviorSystem.\n * \n * This bridges the entity.use() pattern with the ECS component-based approach:\n * - Reads options from entity behaviorRefs\n * - Initializes ThrusterMovementComponent and ThrusterInputComponent on entities\n * - Creates FSM lazily and attaches to BehaviorRef for access via handle.getFSM()\n * - Delegates physics to ThrusterMovementBehavior\n * \n * NOTE: Input is controlled by the user via entity.onUpdate() - set entity.input.thrust/rotate\n */\nclass ThrusterBehaviorSystem implements BehaviorSystem {\n\tprivate movementBehavior: ThrusterMovementBehavior;\n\n\tconstructor(private world: any) {\n\t\tthis.movementBehavior = new ThrusterMovementBehavior(world);\n\t}\n\n\tupdate(ecs: IWorld, delta: number): void {\n\t\tif (!this.world?.collisionMap) return;\n\n\t\t// Initialize ECS components on entities with thruster behavior refs\n\t\tfor (const [, entity] of this.world.collisionMap) {\n\t\t\tconst gameEntity = entity as any;\n\t\t\t\n\t\t\tif (typeof gameEntity.getBehaviorRefs !== 'function') continue;\n\t\t\t\n\t\t\tconst refs = gameEntity.getBehaviorRefs();\n\t\t\tconst thrusterRef = refs.find((r: any) => \n\t\t\t\tr.descriptor.key === Symbol.for('zylem:behavior:thruster')\n\t\t\t);\n\t\t\t\n\t\t\tif (!thrusterRef || !gameEntity.body) continue;\n\n\t\t\tconst options = thrusterRef.options as ThrusterBehaviorOptions;\n\n\t\t\t// Ensure entity has thruster components (initialized once)\n\t\t\tif (!gameEntity.thruster) {\n\t\t\t\tgameEntity.thruster = {\n\t\t\t\t\tlinearThrust: options.linearThrust,\n\t\t\t\t\tangularThrust: options.angularThrust,\n\t\t\t\t} as ThrusterMovementComponent;\n\t\t\t}\n\n\t\t\tif (!gameEntity.$thruster) {\n\t\t\t\tgameEntity.$thruster = {\n\t\t\t\t\tthrust: 0,\n\t\t\t\t\trotate: 0,\n\t\t\t\t} as ThrusterInputComponent;\n\t\t\t}\n\n\t\t\tif (!gameEntity.physics) {\n\t\t\t\tgameEntity.physics = { body: gameEntity.body };\n\t\t\t}\n\n\t\t\t// Create FSM lazily and attach to the BehaviorRef for handle.getFSM()\n\t\t\tif (!thrusterRef.fsm && gameEntity.$thruster) {\n\t\t\t\tthrusterRef.fsm = new ThrusterFSM({ input: gameEntity.$thruster });\n\t\t\t}\n\n\t\t\t// Update FSM to sync state with input (auto-transitions)\n\t\t\tif (thrusterRef.fsm && gameEntity.$thruster) {\n\t\t\t\tthrusterRef.fsm.update({\n\t\t\t\t\tthrust: gameEntity.$thruster.thrust,\n\t\t\t\t\trotate: gameEntity.$thruster.rotate,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Delegate to the existing movement behavior\n\t\tthis.movementBehavior.update(delta);\n\t}\n\n\tdestroy(_ecs: IWorld): void {\n\t\t// Cleanup if needed\n\t}\n}\n\n/**\n * ThrusterBehavior - typed descriptor for thruster movement.\n * \n * Uses the existing ThrusterMovementBehavior under the hood.\n * \n * @example\n * ```typescript\n * import { ThrusterBehavior } from \"@zylem/game-lib\";\n * \n * const ship = createSprite({ ... });\n * ship.use(ThrusterBehavior, { linearThrust: 15, angularThrust: 8 });\n * ```\n */\nexport const ThrusterBehavior = defineBehavior<ThrusterBehaviorOptions, Record<string, never>, ThrusterEntity>({\n\tname: 'thruster',\n\tdefaultOptions,\n\tsystemFactory: (ctx) => new ThrusterBehaviorSystem(ctx.world),\n});\n"],"mappings":"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","names":["ThrusterState","ThrusterEvent"]}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { b as BehaviorDescriptor } from '../behavior-descriptor-BWNWmIjv.js';
|
|
2
|
+
import { StateMachine } from 'typescript-fsm';
|
|
3
|
+
import 'bitecs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WorldBoundary2DFSM
|
|
7
|
+
*
|
|
8
|
+
* Minimal FSM + extended state to track which world boundaries were hit.
|
|
9
|
+
*
|
|
10
|
+
* Notes:
|
|
11
|
+
* - "Hit boundaries" is inherently a *set* (can hit left+bottom in one frame),
|
|
12
|
+
* so we store it as extended state (`lastHits`) rather than a single FSM state.
|
|
13
|
+
* - The FSM state is still useful for coarse status like "inside" vs "touching".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type WorldBoundary2DHit = 'top' | 'bottom' | 'left' | 'right';
|
|
17
|
+
type WorldBoundary2DHits = Record<WorldBoundary2DHit, boolean>;
|
|
18
|
+
interface WorldBoundary2DPosition {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}
|
|
22
|
+
interface WorldBoundary2DBounds {
|
|
23
|
+
top: number;
|
|
24
|
+
bottom: number;
|
|
25
|
+
left: number;
|
|
26
|
+
right: number;
|
|
27
|
+
}
|
|
28
|
+
declare enum WorldBoundary2DState {
|
|
29
|
+
Inside = "inside",
|
|
30
|
+
Touching = "touching"
|
|
31
|
+
}
|
|
32
|
+
declare enum WorldBoundary2DEvent {
|
|
33
|
+
EnterInside = "enter-inside",
|
|
34
|
+
TouchBoundary = "touch-boundary"
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Compute which boundaries are being hit for a position and bounds.
|
|
38
|
+
* This matches the semantics of the legacy `boundary2d` behavior:
|
|
39
|
+
* - left hit if x <= left
|
|
40
|
+
* - right hit if x >= right
|
|
41
|
+
* - bottom hit if y <= bottom
|
|
42
|
+
* - top hit if y >= top
|
|
43
|
+
*/
|
|
44
|
+
declare function computeWorldBoundary2DHits(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits;
|
|
45
|
+
declare function hasAnyWorldBoundary2DHit(hits: WorldBoundary2DHits): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* FSM wrapper with "extended state" (lastHits / lastPosition).
|
|
48
|
+
* Systems should call `update(...)` once per frame.
|
|
49
|
+
*/
|
|
50
|
+
declare class WorldBoundary2DFSM {
|
|
51
|
+
readonly machine: StateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;
|
|
52
|
+
private lastHits;
|
|
53
|
+
private lastPosition;
|
|
54
|
+
private lastUpdatedAtMs;
|
|
55
|
+
constructor();
|
|
56
|
+
getState(): WorldBoundary2DState;
|
|
57
|
+
/**
|
|
58
|
+
* Returns the last computed hits (always available after first update call).
|
|
59
|
+
*/
|
|
60
|
+
getLastHits(): WorldBoundary2DHits;
|
|
61
|
+
/**
|
|
62
|
+
* Returns adjusted movement values based on boundary hits.
|
|
63
|
+
* If the entity is touching a boundary and trying to move further into it,
|
|
64
|
+
* that axis component is zeroed out.
|
|
65
|
+
*
|
|
66
|
+
* @param moveX - The desired X movement
|
|
67
|
+
* @param moveY - The desired Y movement
|
|
68
|
+
* @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed
|
|
69
|
+
*/
|
|
70
|
+
getMovement(moveX: number, moveY: number): {
|
|
71
|
+
moveX: number;
|
|
72
|
+
moveY: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Returns the last position passed to `update`, if any.
|
|
76
|
+
*/
|
|
77
|
+
getLastPosition(): WorldBoundary2DPosition | null;
|
|
78
|
+
/**
|
|
79
|
+
* Best-effort timestamp (ms) of the last `update(...)` call.
|
|
80
|
+
* This is optional metadata; systems can ignore it.
|
|
81
|
+
*/
|
|
82
|
+
getLastUpdatedAtMs(): number | null;
|
|
83
|
+
/**
|
|
84
|
+
* Update FSM + extended state based on current position and bounds.
|
|
85
|
+
* Returns the computed hits for convenience.
|
|
86
|
+
*/
|
|
87
|
+
update(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits;
|
|
88
|
+
private dispatch;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface WorldBoundary2DOptions {
|
|
92
|
+
/**
|
|
93
|
+
* World boundaries (in world units).
|
|
94
|
+
* - left hit if x <= left
|
|
95
|
+
* - right hit if x >= right
|
|
96
|
+
* - bottom hit if y <= bottom
|
|
97
|
+
* - top hit if y >= top
|
|
98
|
+
*/
|
|
99
|
+
boundaries: WorldBoundary2DBounds;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Handle methods provided by WorldBoundary2DBehavior
|
|
103
|
+
*/
|
|
104
|
+
interface WorldBoundary2DHandle {
|
|
105
|
+
/**
|
|
106
|
+
* Get the last computed boundary hits.
|
|
107
|
+
* Returns null until entity is spawned and FSM is initialized.
|
|
108
|
+
*/
|
|
109
|
+
getLastHits(): WorldBoundary2DHits | null;
|
|
110
|
+
/**
|
|
111
|
+
* Get adjusted movement values based on boundary hits.
|
|
112
|
+
* Zeros out movement into boundaries the entity is touching.
|
|
113
|
+
*/
|
|
114
|
+
getMovement(moveX: number, moveY: number): {
|
|
115
|
+
moveX: number;
|
|
116
|
+
moveY: number;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* WorldBoundary2DBehavior
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* import { WorldBoundary2DBehavior } from "@zylem/game-lib";
|
|
125
|
+
*
|
|
126
|
+
* const ship = createSprite({ ... });
|
|
127
|
+
* const boundary = ship.use(WorldBoundary2DBehavior, {
|
|
128
|
+
* boundaries: { left: -10, right: 10, bottom: -7.5, top: 7.5 },
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* ship.onUpdate(({ me }) => {
|
|
132
|
+
* let moveX = ..., moveY = ...;
|
|
133
|
+
* const hits = boundary.getLastHits(); // Fully typed!
|
|
134
|
+
* ({ moveX, moveY } = boundary.getMovement(moveX, moveY));
|
|
135
|
+
* me.moveXY(moveX, moveY);
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare const WorldBoundary2DBehavior: BehaviorDescriptor<WorldBoundary2DOptions, WorldBoundary2DHandle, unknown>;
|
|
140
|
+
|
|
141
|
+
export { WorldBoundary2DBehavior, type WorldBoundary2DBounds, WorldBoundary2DEvent, WorldBoundary2DFSM, type WorldBoundary2DHandle, type WorldBoundary2DHit, type WorldBoundary2DHits, type WorldBoundary2DOptions, type WorldBoundary2DPosition, WorldBoundary2DState, computeWorldBoundary2DHits, hasAnyWorldBoundary2DHit };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// src/lib/behaviors/behavior-descriptor.ts
|
|
2
|
+
function defineBehavior(config) {
|
|
3
|
+
return {
|
|
4
|
+
key: /* @__PURE__ */ Symbol.for(`zylem:behavior:${config.name}`),
|
|
5
|
+
defaultOptions: config.defaultOptions,
|
|
6
|
+
systemFactory: config.systemFactory,
|
|
7
|
+
createHandle: config.createHandle
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts
|
|
12
|
+
import { StateMachine, t } from "typescript-fsm";
|
|
13
|
+
var WorldBoundary2DState = /* @__PURE__ */ ((WorldBoundary2DState2) => {
|
|
14
|
+
WorldBoundary2DState2["Inside"] = "inside";
|
|
15
|
+
WorldBoundary2DState2["Touching"] = "touching";
|
|
16
|
+
return WorldBoundary2DState2;
|
|
17
|
+
})(WorldBoundary2DState || {});
|
|
18
|
+
var WorldBoundary2DEvent = /* @__PURE__ */ ((WorldBoundary2DEvent2) => {
|
|
19
|
+
WorldBoundary2DEvent2["EnterInside"] = "enter-inside";
|
|
20
|
+
WorldBoundary2DEvent2["TouchBoundary"] = "touch-boundary";
|
|
21
|
+
return WorldBoundary2DEvent2;
|
|
22
|
+
})(WorldBoundary2DEvent || {});
|
|
23
|
+
function computeWorldBoundary2DHits(position, bounds) {
|
|
24
|
+
const hits = {
|
|
25
|
+
top: false,
|
|
26
|
+
bottom: false,
|
|
27
|
+
left: false,
|
|
28
|
+
right: false
|
|
29
|
+
};
|
|
30
|
+
if (position.x <= bounds.left) hits.left = true;
|
|
31
|
+
else if (position.x >= bounds.right) hits.right = true;
|
|
32
|
+
if (position.y <= bounds.bottom) hits.bottom = true;
|
|
33
|
+
else if (position.y >= bounds.top) hits.top = true;
|
|
34
|
+
return hits;
|
|
35
|
+
}
|
|
36
|
+
function hasAnyWorldBoundary2DHit(hits) {
|
|
37
|
+
return !!(hits.left || hits.right || hits.top || hits.bottom);
|
|
38
|
+
}
|
|
39
|
+
var WorldBoundary2DFSM = class {
|
|
40
|
+
machine;
|
|
41
|
+
lastHits = { top: false, bottom: false, left: false, right: false };
|
|
42
|
+
lastPosition = null;
|
|
43
|
+
lastUpdatedAtMs = null;
|
|
44
|
+
constructor() {
|
|
45
|
+
this.machine = new StateMachine(
|
|
46
|
+
"inside" /* Inside */,
|
|
47
|
+
[
|
|
48
|
+
t("inside" /* Inside */, "touch-boundary" /* TouchBoundary */, "touching" /* Touching */),
|
|
49
|
+
t("touching" /* Touching */, "enter-inside" /* EnterInside */, "inside" /* Inside */),
|
|
50
|
+
// Self transitions (no-ops)
|
|
51
|
+
t("inside" /* Inside */, "enter-inside" /* EnterInside */, "inside" /* Inside */),
|
|
52
|
+
t("touching" /* Touching */, "touch-boundary" /* TouchBoundary */, "touching" /* Touching */)
|
|
53
|
+
]
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
getState() {
|
|
57
|
+
return this.machine.getState();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns the last computed hits (always available after first update call).
|
|
61
|
+
*/
|
|
62
|
+
getLastHits() {
|
|
63
|
+
return this.lastHits;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns adjusted movement values based on boundary hits.
|
|
67
|
+
* If the entity is touching a boundary and trying to move further into it,
|
|
68
|
+
* that axis component is zeroed out.
|
|
69
|
+
*
|
|
70
|
+
* @param moveX - The desired X movement
|
|
71
|
+
* @param moveY - The desired Y movement
|
|
72
|
+
* @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed
|
|
73
|
+
*/
|
|
74
|
+
getMovement(moveX, moveY) {
|
|
75
|
+
const hits = this.lastHits;
|
|
76
|
+
let adjustedX = moveX;
|
|
77
|
+
let adjustedY = moveY;
|
|
78
|
+
if (hits.left && moveX < 0 || hits.right && moveX > 0) {
|
|
79
|
+
adjustedX = 0;
|
|
80
|
+
}
|
|
81
|
+
if (hits.bottom && moveY < 0 || hits.top && moveY > 0) {
|
|
82
|
+
adjustedY = 0;
|
|
83
|
+
}
|
|
84
|
+
return { moveX: adjustedX, moveY: adjustedY };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns the last position passed to `update`, if any.
|
|
88
|
+
*/
|
|
89
|
+
getLastPosition() {
|
|
90
|
+
return this.lastPosition;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Best-effort timestamp (ms) of the last `update(...)` call.
|
|
94
|
+
* This is optional metadata; systems can ignore it.
|
|
95
|
+
*/
|
|
96
|
+
getLastUpdatedAtMs() {
|
|
97
|
+
return this.lastUpdatedAtMs;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Update FSM + extended state based on current position and bounds.
|
|
101
|
+
* Returns the computed hits for convenience.
|
|
102
|
+
*/
|
|
103
|
+
update(position, bounds) {
|
|
104
|
+
const hits = computeWorldBoundary2DHits(position, bounds);
|
|
105
|
+
this.lastHits = hits;
|
|
106
|
+
this.lastPosition = { x: position.x, y: position.y };
|
|
107
|
+
this.lastUpdatedAtMs = Date.now();
|
|
108
|
+
if (hasAnyWorldBoundary2DHit(hits)) {
|
|
109
|
+
this.dispatch("touch-boundary" /* TouchBoundary */);
|
|
110
|
+
} else {
|
|
111
|
+
this.dispatch("enter-inside" /* EnterInside */);
|
|
112
|
+
}
|
|
113
|
+
return hits;
|
|
114
|
+
}
|
|
115
|
+
dispatch(event) {
|
|
116
|
+
if (this.machine.can(event)) {
|
|
117
|
+
this.machine.dispatch(event);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/lib/behaviors/world-boundary-2d/world-boundary-2d.descriptor.ts
|
|
123
|
+
var defaultOptions = {
|
|
124
|
+
boundaries: { top: 0, bottom: 0, left: 0, right: 0 }
|
|
125
|
+
};
|
|
126
|
+
function createWorldBoundary2DHandle(ref) {
|
|
127
|
+
return {
|
|
128
|
+
getLastHits: () => {
|
|
129
|
+
const fsm = ref.fsm;
|
|
130
|
+
return fsm?.getLastHits() ?? null;
|
|
131
|
+
},
|
|
132
|
+
getMovement: (moveX, moveY) => {
|
|
133
|
+
const fsm = ref.fsm;
|
|
134
|
+
return fsm?.getMovement(moveX, moveY) ?? { moveX, moveY };
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
var WorldBoundary2DSystem = class {
|
|
139
|
+
constructor(world) {
|
|
140
|
+
this.world = world;
|
|
141
|
+
}
|
|
142
|
+
update(_ecs, _delta) {
|
|
143
|
+
if (!this.world?.collisionMap) return;
|
|
144
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
145
|
+
const gameEntity = entity;
|
|
146
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
147
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
148
|
+
const boundaryRef = refs.find(
|
|
149
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:world-boundary-2d")
|
|
150
|
+
);
|
|
151
|
+
if (!boundaryRef || !gameEntity.body) continue;
|
|
152
|
+
const options = boundaryRef.options;
|
|
153
|
+
if (!boundaryRef.fsm) {
|
|
154
|
+
boundaryRef.fsm = new WorldBoundary2DFSM();
|
|
155
|
+
}
|
|
156
|
+
const body = gameEntity.body;
|
|
157
|
+
const pos = body.translation();
|
|
158
|
+
boundaryRef.fsm.update(
|
|
159
|
+
{ x: pos.x, y: pos.y },
|
|
160
|
+
options.boundaries
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
destroy(_ecs) {
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var WorldBoundary2DBehavior = defineBehavior({
|
|
168
|
+
name: "world-boundary-2d",
|
|
169
|
+
defaultOptions,
|
|
170
|
+
systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),
|
|
171
|
+
createHandle: createWorldBoundary2DHandle
|
|
172
|
+
});
|
|
173
|
+
export {
|
|
174
|
+
WorldBoundary2DBehavior,
|
|
175
|
+
WorldBoundary2DEvent,
|
|
176
|
+
WorldBoundary2DFSM,
|
|
177
|
+
WorldBoundary2DState,
|
|
178
|
+
computeWorldBoundary2DHits,
|
|
179
|
+
hasAnyWorldBoundary2DHit
|
|
180
|
+
};
|
|
181
|
+
//# sourceMappingURL=world-boundary-2d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/behaviors/behavior-descriptor.ts","../../src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts","../../src/lib/behaviors/world-boundary-2d/world-boundary-2d.descriptor.ts"],"sourcesContent":["/**\n * BehaviorDescriptor\n *\n * Type-safe behavior descriptors that provide options inference.\n * Used with entity.use() to declaratively attach behaviors to entities.\n *\n * Each behavior can define its own handle type via `createHandle`,\n * providing behavior-specific methods with full type safety.\n */\n\nimport type { BehaviorSystemFactory } from './behavior-system';\n\n/**\n * Base handle returned by entity.use() for lazy access to behavior runtime.\n * FSM is null until entity is spawned and components are initialized.\n */\nexport interface BaseBehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** Get the FSM instance (null until entity is spawned) */\n getFSM(): any | null;\n /** Get the current options */\n getOptions(): O;\n /** Access the underlying behavior ref */\n readonly ref: BehaviorRef<O>;\n}\n\n/**\n * Reference to a behavior stored on an entity\n */\nexport interface BehaviorRef<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** The behavior descriptor */\n descriptor: BehaviorDescriptor<O, any>;\n /** Merged options (defaults + overrides) */\n options: O;\n /** Optional FSM instance - set lazily when entity is spawned */\n fsm?: any;\n}\n\n/**\n * A typed behavior descriptor that associates a symbol key with:\n * - Default options (providing type inference)\n * - A system factory to create the behavior system\n * - An optional handle factory for behavior-specific methods\n */\nexport interface BehaviorDescriptor<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Unique symbol identifying this behavior */\n readonly key: symbol;\n /** Default options (used for type inference) */\n readonly defaultOptions: O;\n /** Factory to create the behavior system */\n readonly systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * These methods are merged into the handle returned by entity.use().\n */\n readonly createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * The full handle type returned by entity.use().\n * Combines base handle with behavior-specific methods.\n */\nexport type BehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n> = BaseBehaviorHandle<O> & H;\n\n/**\n * Configuration for defining a new behavior\n */\nexport interface DefineBehaviorConfig<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Human-readable name for debugging */\n name: string;\n /** Default options - these define the type */\n defaultOptions: O;\n /** Factory function to create the system */\n systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * The returned object is merged into the handle returned by entity.use().\n *\n * @example\n * ```typescript\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX, moveY) => ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * ```\n */\n createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * Define a typed behavior descriptor.\n *\n * @example\n * ```typescript\n * export const WorldBoundary2DBehavior = defineBehavior({\n * name: 'world-boundary-2d',\n * defaultOptions: { boundaries: { top: 0, bottom: 0, left: 0, right: 0 } },\n * systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX: number, moveY: number) =>\n * ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * });\n *\n * // Usage - handle has getLastHits and getMovement with full types\n * const boundary = ship.use(WorldBoundary2DBehavior, { ... });\n * const hits = boundary.getLastHits(); // Fully typed!\n * ```\n */\nexport function defineBehavior<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n>(\n config: DefineBehaviorConfig<O, H, I>,\n): BehaviorDescriptor<O, H, I> {\n return {\n key: Symbol.for(`zylem:behavior:${config.name}`),\n defaultOptions: config.defaultOptions,\n systemFactory: config.systemFactory,\n createHandle: config.createHandle,\n };\n}\n","/**\n * WorldBoundary2DFSM\n *\n * Minimal FSM + extended state to track which world boundaries were hit.\n *\n * Notes:\n * - \"Hit boundaries\" is inherently a *set* (can hit left+bottom in one frame),\n * so we store it as extended state (`lastHits`) rather than a single FSM state.\n * - The FSM state is still useful for coarse status like \"inside\" vs \"touching\".\n */\n\nimport { StateMachine, t } from 'typescript-fsm';\n\nexport type WorldBoundary2DHit = 'top' | 'bottom' | 'left' | 'right';\nexport type WorldBoundary2DHits = Record<WorldBoundary2DHit, boolean>;\n\nexport interface WorldBoundary2DPosition {\n\tx: number;\n\ty: number;\n}\n\nexport interface WorldBoundary2DBounds {\n\ttop: number;\n\tbottom: number;\n\tleft: number;\n\tright: number;\n}\n\nexport enum WorldBoundary2DState {\n\tInside = 'inside',\n\tTouching = 'touching',\n}\n\nexport enum WorldBoundary2DEvent {\n\tEnterInside = 'enter-inside',\n\tTouchBoundary = 'touch-boundary',\n}\n\n/**\n * Compute which boundaries are being hit for a position and bounds.\n * This matches the semantics of the legacy `boundary2d` behavior:\n * - left hit if x <= left\n * - right hit if x >= right\n * - bottom hit if y <= bottom\n * - top hit if y >= top\n */\nexport function computeWorldBoundary2DHits(\n\tposition: WorldBoundary2DPosition,\n\tbounds: WorldBoundary2DBounds\n): WorldBoundary2DHits {\n\tconst hits: WorldBoundary2DHits = {\n\t\ttop: false,\n\t\tbottom: false,\n\t\tleft: false,\n\t\tright: false,\n\t};\n\n\tif (position.x <= bounds.left) hits.left = true;\n\telse if (position.x >= bounds.right) hits.right = true;\n\n\tif (position.y <= bounds.bottom) hits.bottom = true;\n\telse if (position.y >= bounds.top) hits.top = true;\n\n\treturn hits;\n}\n\nexport function hasAnyWorldBoundary2DHit(hits: WorldBoundary2DHits): boolean {\n\treturn !!(hits.left || hits.right || hits.top || hits.bottom);\n}\n\n/**\n * FSM wrapper with \"extended state\" (lastHits / lastPosition).\n * Systems should call `update(...)` once per frame.\n */\nexport class WorldBoundary2DFSM {\n\tpublic readonly machine: StateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;\n\n\tprivate lastHits: WorldBoundary2DHits = { top: false, bottom: false, left: false, right: false };\n\tprivate lastPosition: WorldBoundary2DPosition | null = null;\n\tprivate lastUpdatedAtMs: number | null = null;\n\n\tconstructor() {\n\t\tthis.machine = new StateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>(\n\t\t\tWorldBoundary2DState.Inside,\n\t\t\t[\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\n\t\t\t\t// Self transitions (no-ops)\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t]\n\t\t);\n\t}\n\n\tgetState(): WorldBoundary2DState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Returns the last computed hits (always available after first update call).\n\t */\n\tgetLastHits(): WorldBoundary2DHits {\n\t\treturn this.lastHits;\n\t}\n\n\t/**\n\t * Returns adjusted movement values based on boundary hits.\n\t * If the entity is touching a boundary and trying to move further into it,\n\t * that axis component is zeroed out.\n\t *\n\t * @param moveX - The desired X movement\n\t * @param moveY - The desired Y movement\n\t * @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number } {\n\t\tconst hits = this.lastHits;\n\n\t\tlet adjustedX = moveX;\n\t\tlet adjustedY = moveY;\n\n\t\t// If moving left and hitting left, or moving right and hitting right, zero X\n\t\tif ((hits.left && moveX < 0) || (hits.right && moveX > 0)) {\n\t\t\tadjustedX = 0;\n\t\t}\n\n\t\t// If moving down and hitting bottom, or moving up and hitting top, zero Y\n\t\tif ((hits.bottom && moveY < 0) || (hits.top && moveY > 0)) {\n\t\t\tadjustedY = 0;\n\t\t}\n\n\t\treturn { moveX: adjustedX, moveY: adjustedY };\n\t}\n\n\t/**\n\t * Returns the last position passed to `update`, if any.\n\t */\n\tgetLastPosition(): WorldBoundary2DPosition | null {\n\t\treturn this.lastPosition;\n\t}\n\n\t/**\n\t * Best-effort timestamp (ms) of the last `update(...)` call.\n\t * This is optional metadata; systems can ignore it.\n\t */\n\tgetLastUpdatedAtMs(): number | null {\n\t\treturn this.lastUpdatedAtMs;\n\t}\n\n\t/**\n\t * Update FSM + extended state based on current position and bounds.\n\t * Returns the computed hits for convenience.\n\t */\n\tupdate(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits {\n\t\tconst hits = computeWorldBoundary2DHits(position, bounds);\n\n\t\tthis.lastHits = hits;\n\t\tthis.lastPosition = { x: position.x, y: position.y };\n\t\tthis.lastUpdatedAtMs = Date.now();\n\n\t\tif (hasAnyWorldBoundary2DHit(hits)) {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.TouchBoundary);\n\t\t} else {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.EnterInside);\n\t\t}\n\n\t\treturn hits;\n\t}\n\n\tprivate dispatch(event: WorldBoundary2DEvent): void {\n\t\tif (this.machine.can(event)) {\n\t\t\tthis.machine.dispatch(event);\n\t\t}\n\t}\n}\n","/**\n * WorldBoundary2DBehavior\n *\n * Tracks which 2D world boundaries an entity is touching.\n * Use `getMovement()` on the behavior handle to adjust movement based on hits.\n *\n * Source of truth: `entity.body` (Rapier rigid body), consistent with other new behaviors.\n */\n\nimport type { IWorld } from 'bitecs';\nimport { defineBehavior, type BehaviorRef } from '../behavior-descriptor';\nimport type { BehaviorSystem } from '../behavior-system';\nimport {\n\tWorldBoundary2DFSM,\n\ttype WorldBoundary2DBounds,\n\ttype WorldBoundary2DHits,\n} from './world-boundary-2d-fsm';\n\nexport interface WorldBoundary2DOptions {\n\t/**\n\t * World boundaries (in world units).\n\t * - left hit if x <= left\n\t * - right hit if x >= right\n\t * - bottom hit if y <= bottom\n\t * - top hit if y >= top\n\t */\n\tboundaries: WorldBoundary2DBounds;\n}\n\n/**\n * Handle methods provided by WorldBoundary2DBehavior\n */\nexport interface WorldBoundary2DHandle {\n\t/**\n\t * Get the last computed boundary hits.\n\t * Returns null until entity is spawned and FSM is initialized.\n\t */\n\tgetLastHits(): WorldBoundary2DHits | null;\n\n\t/**\n\t * Get adjusted movement values based on boundary hits.\n\t * Zeros out movement into boundaries the entity is touching.\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number };\n}\n\nconst defaultOptions: WorldBoundary2DOptions = {\n\tboundaries: { top: 0, bottom: 0, left: 0, right: 0 },\n};\n\n/**\n * Creates behavior-specific handle methods for WorldBoundary2DBehavior.\n */\nfunction createWorldBoundary2DHandle(\n\tref: BehaviorRef<WorldBoundary2DOptions>\n): WorldBoundary2DHandle {\n\treturn {\n\t\tgetLastHits: () => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getLastHits() ?? null;\n\t\t},\n\t\tgetMovement: (moveX: number, moveY: number) => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getMovement(moveX, moveY) ?? { moveX, moveY };\n\t\t},\n\t};\n}\n\n/**\n * WorldBoundary2DSystem\n *\n * Stage-level system that:\n * - finds entities with this behavior attached\n * - computes and tracks boundary hits using the FSM\n */\nclass WorldBoundary2DSystem implements BehaviorSystem {\n\tconstructor(private world: any) {}\n\n\tupdate(_ecs: IWorld, _delta: number): void {\n\t\tif (!this.world?.collisionMap) return;\n\n\t\tfor (const [, entity] of this.world.collisionMap) {\n\t\t\tconst gameEntity = entity as any;\n\n\t\t\tif (typeof gameEntity.getBehaviorRefs !== 'function') continue;\n\n\t\t\tconst refs = gameEntity.getBehaviorRefs();\n\t\t\tconst boundaryRef = refs.find(\n\t\t\t\t(r: any) => r.descriptor.key === Symbol.for('zylem:behavior:world-boundary-2d')\n\t\t\t);\n\n\t\t\tif (!boundaryRef || !gameEntity.body) continue;\n\n\t\t\tconst options = boundaryRef.options as WorldBoundary2DOptions;\n\n\t\t\t// Create FSM lazily on first update after spawn\n\t\t\tif (!boundaryRef.fsm) {\n\t\t\t\tboundaryRef.fsm = new WorldBoundary2DFSM();\n\t\t\t}\n\n\t\t\tconst body = gameEntity.body;\n\t\t\tconst pos = body.translation();\n\n\t\t\t// Update FSM with current position - consumers use getMovement() to act on hits\n\t\t\tboundaryRef.fsm.update(\n\t\t\t\t{ x: pos.x, y: pos.y },\n\t\t\t\toptions.boundaries\n\t\t\t);\n\t\t}\n\t}\n\n\tdestroy(_ecs: IWorld): void {\n\t\t// No explicit cleanup required (FSMs are stored on behavior refs)\n\t}\n}\n\n/**\n * WorldBoundary2DBehavior\n *\n * @example\n * ```ts\n * import { WorldBoundary2DBehavior } from \"@zylem/game-lib\";\n *\n * const ship = createSprite({ ... });\n * const boundary = ship.use(WorldBoundary2DBehavior, {\n * boundaries: { left: -10, right: 10, bottom: -7.5, top: 7.5 },\n * });\n *\n * ship.onUpdate(({ me }) => {\n * let moveX = ..., moveY = ...;\n * const hits = boundary.getLastHits(); // Fully typed!\n * ({ moveX, moveY } = boundary.getMovement(moveX, moveY));\n * me.moveXY(moveX, moveY);\n * });\n * ```\n */\nexport const WorldBoundary2DBehavior = defineBehavior({\n\tname: 'world-boundary-2d',\n\tdefaultOptions,\n\tsystemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),\n\tcreateHandle: createWorldBoundary2DHandle,\n});\n"],"mappings":"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","names":["WorldBoundary2DState","WorldBoundary2DEvent"]}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { IWorld } from 'bitecs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BehaviorSystem Interface
|
|
5
|
+
*
|
|
6
|
+
* Base interface for ECS-based behavior systems that run at the stage level.
|
|
7
|
+
* Systems query entities with matching components and process them each frame.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A behavior system that processes entities with specific components.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* class ThrusterMovementSystem implements BehaviorSystem {
|
|
16
|
+
* update(ecs: IWorld, delta: number): void {
|
|
17
|
+
* // Query and process entities with thruster components
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
interface BehaviorSystem {
|
|
23
|
+
/** Called once per frame with ECS world and delta time */
|
|
24
|
+
update(ecs: IWorld, delta: number): void;
|
|
25
|
+
/** Optional cleanup when stage is destroyed */
|
|
26
|
+
destroy?(ecs: IWorld): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Factory function that creates a BehaviorSystem.
|
|
30
|
+
* Receives the stage for access to world, scene, etc.
|
|
31
|
+
*/
|
|
32
|
+
type BehaviorSystemFactory<T extends BehaviorSystem = BehaviorSystem> = (stage: {
|
|
33
|
+
world: any;
|
|
34
|
+
ecs: IWorld;
|
|
35
|
+
scene: any;
|
|
36
|
+
}) => T;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* BehaviorDescriptor
|
|
40
|
+
*
|
|
41
|
+
* Type-safe behavior descriptors that provide options inference.
|
|
42
|
+
* Used with entity.use() to declaratively attach behaviors to entities.
|
|
43
|
+
*
|
|
44
|
+
* Each behavior can define its own handle type via `createHandle`,
|
|
45
|
+
* providing behavior-specific methods with full type safety.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Base handle returned by entity.use() for lazy access to behavior runtime.
|
|
50
|
+
* FSM is null until entity is spawned and components are initialized.
|
|
51
|
+
*/
|
|
52
|
+
interface BaseBehaviorHandle<O extends Record<string, any> = Record<string, any>> {
|
|
53
|
+
/** Get the FSM instance (null until entity is spawned) */
|
|
54
|
+
getFSM(): any | null;
|
|
55
|
+
/** Get the current options */
|
|
56
|
+
getOptions(): O;
|
|
57
|
+
/** Access the underlying behavior ref */
|
|
58
|
+
readonly ref: BehaviorRef<O>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reference to a behavior stored on an entity
|
|
62
|
+
*/
|
|
63
|
+
interface BehaviorRef<O extends Record<string, any> = Record<string, any>> {
|
|
64
|
+
/** The behavior descriptor */
|
|
65
|
+
descriptor: BehaviorDescriptor<O, any>;
|
|
66
|
+
/** Merged options (defaults + overrides) */
|
|
67
|
+
options: O;
|
|
68
|
+
/** Optional FSM instance - set lazily when entity is spawned */
|
|
69
|
+
fsm?: any;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A typed behavior descriptor that associates a symbol key with:
|
|
73
|
+
* - Default options (providing type inference)
|
|
74
|
+
* - A system factory to create the behavior system
|
|
75
|
+
* - An optional handle factory for behavior-specific methods
|
|
76
|
+
*/
|
|
77
|
+
interface BehaviorDescriptor<O extends Record<string, any> = Record<string, any>, H extends Record<string, any> = Record<string, never>, I = unknown> {
|
|
78
|
+
/** Unique symbol identifying this behavior */
|
|
79
|
+
readonly key: symbol;
|
|
80
|
+
/** Default options (used for type inference) */
|
|
81
|
+
readonly defaultOptions: O;
|
|
82
|
+
/** Factory to create the behavior system */
|
|
83
|
+
readonly systemFactory: BehaviorSystemFactory;
|
|
84
|
+
/**
|
|
85
|
+
* Optional factory to create behavior-specific handle methods.
|
|
86
|
+
* These methods are merged into the handle returned by entity.use().
|
|
87
|
+
*/
|
|
88
|
+
readonly createHandle?: (ref: BehaviorRef<O>) => H;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The full handle type returned by entity.use().
|
|
92
|
+
* Combines base handle with behavior-specific methods.
|
|
93
|
+
*/
|
|
94
|
+
type BehaviorHandle<O extends Record<string, any> = Record<string, any>, H extends Record<string, any> = Record<string, never>> = BaseBehaviorHandle<O> & H;
|
|
95
|
+
/**
|
|
96
|
+
* Configuration for defining a new behavior
|
|
97
|
+
*/
|
|
98
|
+
interface DefineBehaviorConfig<O extends Record<string, any>, H extends Record<string, any> = Record<string, never>, I = unknown> {
|
|
99
|
+
/** Human-readable name for debugging */
|
|
100
|
+
name: string;
|
|
101
|
+
/** Default options - these define the type */
|
|
102
|
+
defaultOptions: O;
|
|
103
|
+
/** Factory function to create the system */
|
|
104
|
+
systemFactory: BehaviorSystemFactory;
|
|
105
|
+
/**
|
|
106
|
+
* Optional factory to create behavior-specific handle methods.
|
|
107
|
+
* The returned object is merged into the handle returned by entity.use().
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* createHandle: (ref) => ({
|
|
112
|
+
* getLastHits: () => ref.fsm?.getLastHits() ?? null,
|
|
113
|
+
* getMovement: (moveX, moveY) => ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },
|
|
114
|
+
* }),
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
createHandle?: (ref: BehaviorRef<O>) => H;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Define a typed behavior descriptor.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* export const WorldBoundary2DBehavior = defineBehavior({
|
|
125
|
+
* name: 'world-boundary-2d',
|
|
126
|
+
* defaultOptions: { boundaries: { top: 0, bottom: 0, left: 0, right: 0 } },
|
|
127
|
+
* systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),
|
|
128
|
+
* createHandle: (ref) => ({
|
|
129
|
+
* getLastHits: () => ref.fsm?.getLastHits() ?? null,
|
|
130
|
+
* getMovement: (moveX: number, moveY: number) =>
|
|
131
|
+
* ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },
|
|
132
|
+
* }),
|
|
133
|
+
* });
|
|
134
|
+
*
|
|
135
|
+
* // Usage - handle has getLastHits and getMovement with full types
|
|
136
|
+
* const boundary = ship.use(WorldBoundary2DBehavior, { ... });
|
|
137
|
+
* const hits = boundary.getLastHits(); // Fully typed!
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
declare function defineBehavior<O extends Record<string, any>, H extends Record<string, any> = Record<string, never>, I = unknown>(config: DefineBehaviorConfig<O, H, I>): BehaviorDescriptor<O, H, I>;
|
|
141
|
+
|
|
142
|
+
export { type BehaviorSystem as B, type DefineBehaviorConfig as D, type BehaviorSystemFactory as a, type BehaviorDescriptor as b, type BehaviorHandle as c, type BehaviorRef as d, defineBehavior as e };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Vector2 } from 'three';
|
|
2
|
-
import {
|
|
3
|
-
import { b as Stage } from './stage-types-
|
|
2
|
+
import { I as GameEntity } from './entity-BpbZqg19.js';
|
|
3
|
+
import { b as Stage } from './stage-types-BFsm3qsZ.js';
|
|
4
4
|
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
5
5
|
import { Static } from '@sinclair/typebox';
|
|
6
6
|
|