@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/ricochet-2d/ricochet-2d-fsm.ts","../../src/lib/behaviors/behavior-descriptor.ts","../../src/lib/behaviors/ricochet-2d/ricochet-2d.descriptor.ts"],"sourcesContent":["/**\n * Ricochet2DFSM\n *\n * FSM + extended state to track ricochet events and results.\n * The FSM state tracks whether a ricochet is currently occurring.\n */\nimport { BaseEntityInterface } from \"../../types/entity-types\";\nimport { StateMachine, t } from 'typescript-fsm';\n\nexport interface Ricochet2DResult {\n\t/** The reflected velocity vector */\n\tvelocity: { x: number; y: number; z?: number };\n\t/** The resulting speed after reflection */\n\tspeed: number;\n\t/** The collision normal used for reflection */\n\tnormal: { x: number; y: number; z?: number };\n}\n\nexport interface Ricochet2DCollisionContext {\n\tentity?: BaseEntityInterface;\n\totherEntity?: BaseEntityInterface;\n\t/** Current velocity of the entity (optional if entity is provided) */\n\tselfVelocity?: { x: number; y: number; z?: number };\n\t/** Contact information from the collision */\n\tcontact: {\n\t\t/** The collision normal */\n\t\tnormal?: { x: number; y: number; z?: number };\n\t\t/**\n\t\t * Optional position where the collision occurred.\n\t\t * If provided, used for precise offset calculation.\n\t\t */\n\t\tposition?: { x: number; y: number; z?: number };\n\t};\n\t/**\n\t * Optional position of the entity that owns this behavior.\n\t * Used with contact.position for offset calculations.\n\t */\n\tselfPosition?: { x: number; y: number; z?: number };\n\t/**\n\t * Optional position of the other entity in the collision.\n\t * Used for paddle-style deflection: offset = (contactY - otherY) / halfHeight.\n\t */\n\totherPosition?: { x: number; y: number; z?: number };\n\t/**\n\t * Optional size of the other entity (e.g., paddle size).\n\t * If provided, used to normalize the offset based on the collision face.\n\t */\n\totherSize?: { x: number; y: number; z?: number };\n}\n\nexport enum Ricochet2DState {\n\tIdle = 'idle',\n\tRicocheting = 'ricocheting',\n}\n\nexport enum Ricochet2DEvent {\n\tStartRicochet = 'start-ricochet',\n\tEndRicochet = 'end-ricochet',\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n\treturn Math.max(min, Math.min(max, value));\n}\n\n/**\n * Callback type for ricochet event listeners.\n */\nexport type RicochetCallback = (result: Ricochet2DResult) => void;\n\n/**\n * FSM wrapper with extended state (lastResult).\n * Systems or consumers call `computeRicochet(...)` when a collision occurs.\n */\nexport class Ricochet2DFSM {\n\tpublic readonly machine: StateMachine<Ricochet2DState, Ricochet2DEvent, never>;\n\n\tprivate lastResult: Ricochet2DResult | null = null;\n\tprivate lastUpdatedAtMs: number | null = null;\n\tprivate currentTimeMs: number = 0;\n\tprivate listeners: Set<RicochetCallback> = new Set();\n\n\tconstructor() {\n\t\tthis.machine = new StateMachine<Ricochet2DState, Ricochet2DEvent, never>(\n\t\t\tRicochet2DState.Idle,\n\t\t\t[\n\t\t\t\tt(Ricochet2DState.Idle, Ricochet2DEvent.StartRicochet, Ricochet2DState.Ricocheting),\n\t\t\t\tt(Ricochet2DState.Ricocheting, Ricochet2DEvent.EndRicochet, Ricochet2DState.Idle),\n\n\t\t\t\t// Self transitions (no-ops)\n\t\t\t\tt(Ricochet2DState.Idle, Ricochet2DEvent.EndRicochet, Ricochet2DState.Idle),\n\t\t\t\tt(Ricochet2DState.Ricocheting, Ricochet2DEvent.StartRicochet, Ricochet2DState.Ricocheting),\n\t\t\t]\n\t\t);\n\t}\n\n\t/**\n\t * Add a listener for ricochet events.\n\t * @returns Unsubscribe function\n\t */\n\taddListener(callback: RicochetCallback): () => void {\n\t\tthis.listeners.add(callback);\n\t\treturn () => this.listeners.delete(callback);\n\t}\n\n\t/**\n\t * Emit result to all listeners.\n\t */\n\tprivate emitToListeners(result: Ricochet2DResult): void {\n\t\tfor (const callback of this.listeners) {\n\t\t\ttry {\n\t\t\t\tcallback(result);\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error('[Ricochet2DFSM] Listener error:', e);\n\t\t\t}\n\t\t}\n\t}\n\n\tgetState(): Ricochet2DState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Returns the last computed ricochet result, or null if none.\n\t */\n\tgetLastResult(): Ricochet2DResult | null {\n\t\treturn this.lastResult;\n\t}\n\n\t/**\n\t * Best-effort timestamp (ms) of the last computation.\n\t */\n\tgetLastUpdatedAtMs(): number | null {\n\t\treturn this.lastUpdatedAtMs;\n\t}\n\n\t/**\n\t * Set current game time (called by system each frame).\n\t * Used for cooldown calculations.\n\t */\n\tsetCurrentTimeMs(timeMs: number): void {\n\t\tthis.currentTimeMs = timeMs;\n\t}\n\n\t/**\n\t * Check if ricochet is on cooldown (to prevent rapid duplicate applications).\n\t * @param cooldownMs Cooldown duration in milliseconds (default: 50ms)\n\t */\n\tisOnCooldown(cooldownMs: number = 50): boolean {\n\t\tif (this.lastUpdatedAtMs === null) return false;\n\t\treturn (this.currentTimeMs - this.lastUpdatedAtMs) < cooldownMs;\n\t}\n\n\t/**\n\t * Reset cooldown state (e.g., on entity respawn).\n\t */\n\tresetCooldown(): void {\n\t\tthis.lastUpdatedAtMs = null;\n\t}\n\n\t/**\n\t * Compute a ricochet result from collision context.\n\t * Returns the result for the consumer to apply, or null if invalid input.\n\t */\n\tcomputeRicochet(\n\t\tctx: Ricochet2DCollisionContext,\n\t\toptions: {\n\t\t\tminSpeed?: number;\n\t\t\tmaxSpeed?: number;\n\t\t\tspeedMultiplier?: number;\n\t\t\treflectionMode?: 'simple' | 'angled';\n\t\t\tmaxAngleDeg?: number;\n\t\t} = {}\n\t): Ricochet2DResult | null {\n\t\tconst {\n\t\t\tminSpeed = 2,\n\t\t\tmaxSpeed = 20,\n\t\t\tspeedMultiplier = 1.05,\n\t\t\treflectionMode = 'angled',\n\t\t\tmaxAngleDeg = 60,\n\t\t} = options;\n\n\t\t// Extract data from entities if provided\n\t\tconst { selfVelocity, selfPosition, otherPosition, otherSize } = this.extractDataFromEntities(ctx);\n\n\t\tif (!selfVelocity) {\n\t\t\tthis.dispatch(Ricochet2DEvent.EndRicochet);\n\t\t\treturn null;\n\t\t}\n\n\t\tconst speed = Math.hypot(selfVelocity.x, selfVelocity.y);\n\t\tif (speed === 0) {\n\t\t\tthis.dispatch(Ricochet2DEvent.EndRicochet);\n\t\t\treturn null;\n\t\t}\n\n\t\t// Compute or extract collision normal\n\t\tconst normal = ctx.contact.normal ?? this.computeNormalFromPositions(selfPosition, otherPosition);\n\t\tif (!normal) {\n\t\t\tthis.dispatch(Ricochet2DEvent.EndRicochet);\n\t\t\treturn null;\n\t\t}\n\n\t\t// Compute basic reflection\n\t\tlet reflected = this.computeBasicReflection(selfVelocity, normal);\n\n\t\t// Apply angled deflection if requested\n\t\tif (reflectionMode === 'angled') {\n\t\t\treflected = this.computeAngledDeflection(\n\t\t\t\tselfVelocity,\n\t\t\t\tnormal,\n\t\t\t\tspeed,\n\t\t\t\tmaxAngleDeg,\n\t\t\t\tspeedMultiplier,\n\t\t\t\tselfPosition,\n\t\t\t\totherPosition,\n\t\t\t\totherSize,\n\t\t\t\tctx.contact.position\n\t\t\t);\n\t\t}\n\n\t\t// Apply final speed constraints\n\t\treflected = this.applySpeedClamp(reflected, minSpeed, maxSpeed);\n\n\t\tconst result: Ricochet2DResult = {\n\t\t\tvelocity: { x: reflected.x, y: reflected.y, z: 0 },\n\t\t\tspeed: Math.hypot(reflected.x, reflected.y),\n\t\t\tnormal: { x: normal.x, y: normal.y, z: 0 },\n\t\t};\n\n\t\tthis.lastResult = result;\n\t\tthis.lastUpdatedAtMs = this.currentTimeMs;\n\t\tthis.dispatch(Ricochet2DEvent.StartRicochet);\n\t\tthis.emitToListeners(result);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Extract velocity, position, and size data from entities or context.\n\t */\n\tprivate extractDataFromEntities(ctx: Ricochet2DCollisionContext) {\n\t\tlet selfVelocity = ctx.selfVelocity;\n\t\tlet selfPosition = ctx.selfPosition;\n\t\tlet otherPosition = ctx.otherPosition;\n\t\tlet otherSize = ctx.otherSize;\n\n\t\tif (ctx.entity?.body) {\n\t\t\tconst vel = ctx.entity.body.linvel();\n\t\t\tselfVelocity = selfVelocity ?? { x: vel.x, y: vel.y, z: vel.z };\n\t\t\tconst pos = ctx.entity.body.translation();\n\t\t\tselfPosition = selfPosition ?? { x: pos.x, y: pos.y, z: pos.z };\n\t\t}\n\n\t\tif (ctx.otherEntity?.body) {\n\t\t\tconst pos = ctx.otherEntity.body.translation();\n\t\t\totherPosition = otherPosition ?? { x: pos.x, y: pos.y, z: pos.z };\n\t\t}\n\n\t\tif (ctx.otherEntity && 'size' in ctx.otherEntity) {\n\t\t\tconst size = (ctx.otherEntity as any).size;\n\t\t\tif (size && typeof size.x === 'number') {\n\t\t\t\totherSize = otherSize ?? { x: size.x, y: size.y, z: size.z };\n\t\t\t}\n\t\t}\n\n\t\treturn { selfVelocity, selfPosition, otherPosition, otherSize };\n\t}\n\n\t/**\n\t * Compute collision normal from entity positions using AABB heuristic.\n\t */\n\tprivate computeNormalFromPositions(\n\t\tselfPosition?: { x: number; y: number; z?: number },\n\t\totherPosition?: { x: number; y: number; z?: number }\n\t): { x: number; y: number; z?: number } | null {\n\t\tif (!selfPosition || !otherPosition) return null;\n\n\t\tconst dx = selfPosition.x - otherPosition.x;\n\t\tconst dy = selfPosition.y - otherPosition.y;\n\n\t\t// Simple \"which face was hit\" logic for box collisions\n\t\tif (Math.abs(dx) > Math.abs(dy)) {\n\t\t\treturn { x: dx > 0 ? 1 : -1, y: 0, z: 0 };\n\t\t} else {\n\t\t\treturn { x: 0, y: dy > 0 ? 1 : -1, z: 0 };\n\t\t}\n\t}\n\n\t/**\n\t * Compute basic reflection using the formula: v' = v - 2(v·n)n\n\t */\n\tprivate computeBasicReflection(\n\t\tvelocity: { x: number; y: number },\n\t\tnormal: { x: number; y: number; z?: number }\n\t): { x: number; y: number } {\n\t\tconst vx = velocity.x;\n\t\tconst vy = velocity.y;\n\t\tconst dotProduct = vx * normal.x + vy * normal.y;\n\n\t\treturn {\n\t\t\tx: vx - 2 * dotProduct * normal.x,\n\t\t\ty: vy - 2 * dotProduct * normal.y,\n\t\t};\n\t}\n\n\t/**\n\t * Compute angled deflection for paddle-style reflections.\n\t */\n\tprivate computeAngledDeflection(\n\t\tvelocity: { x: number; y: number },\n\t\tnormal: { x: number; y: number; z?: number },\n\t\tspeed: number,\n\t\tmaxAngleDeg: number,\n\t\tspeedMultiplier: number,\n\t\tselfPosition?: { x: number; y: number; z?: number },\n\t\totherPosition?: { x: number; y: number; z?: number },\n\t\totherSize?: { x: number; y: number; z?: number },\n\t\tcontactPosition?: { x: number; y: number; z?: number }\n\t): { x: number; y: number } {\n\t\tconst maxAngleRad = (maxAngleDeg * Math.PI) / 180;\n\n\t\t// Tangent vector (perpendicular to normal)\n\t\tlet tx = -normal.y;\n\t\tlet ty = normal.x;\n\n\t\t// Ensure tangent points in positive direction of the deflection axis\n\t\t// so that positive offset (right/top) results in positive deflection\n\t\tif (Math.abs(normal.x) > Math.abs(normal.y)) {\n\t\t\t// Vertical face (Normal is X-aligned). Deflection axis is Y.\n\t\t\t// We want ty > 0.\n\t\t\tif (ty < 0) {\n\t\t\t\ttx = -tx;\n\t\t\t\tty = -ty;\n\t\t\t}\n\t\t} else {\n\t\t\t// Horizontal face (Normal is Y-aligned). Deflection axis is X.\n\t\t\t// We want tx > 0.\n\t\t\tif (tx < 0) {\n\t\t\t\ttx = -tx;\n\t\t\t\tty = -ty;\n\t\t\t}\n\t\t}\n\n\t\t// Compute offset based on hit position\n\t\tconst offset = this.computeHitOffset(\n\t\t\tvelocity,\n\t\t\tnormal,\n\t\t\tspeed,\n\t\t\ttx,\n\t\t\tty,\n\t\t\tselfPosition,\n\t\t\totherPosition,\n\t\t\totherSize,\n\t\t\tcontactPosition\n\t\t);\n\n\t\tconst angle = clamp(offset, -1, 1) * maxAngleRad;\n\n\t\tconst cosA = Math.cos(angle);\n\t\tconst sinA = Math.sin(angle);\n\n\t\tconst newSpeed = speed * speedMultiplier;\n\n\t\treturn {\n\t\t\tx: newSpeed * (normal.x * cosA + tx * sinA),\n\t\t\ty: newSpeed * (normal.y * cosA + ty * sinA),\n\t\t};\n\t}\n\n\t/**\n\t * Compute hit offset for angled deflection (-1 to 1).\n\t */\n\tprivate computeHitOffset(\n\t\tvelocity: { x: number; y: number },\n\t\tnormal: { x: number; y: number; z?: number },\n\t\tspeed: number,\n\t\ttx: number,\n\t\tty: number,\n\t\tselfPosition?: { x: number; y: number; z?: number },\n\t\totherPosition?: { x: number; y: number; z?: number },\n\t\totherSize?: { x: number; y: number; z?: number },\n\t\tcontactPosition?: { x: number; y: number; z?: number }\n\t): number {\n\t\t// Use position-based offset if available\n\t\tif (otherPosition && otherSize) {\n\t\t\tconst useY = Math.abs(normal.x) > Math.abs(normal.y);\n\t\t\tconst halfExtent = useY ? otherSize.y / 2 : otherSize.x / 2;\n\n\t\t\tif (useY) {\n\t\t\t\tconst selfY = selfPosition?.y ?? contactPosition?.y ?? 0;\n\t\t\t\tconst paddleY = otherPosition.y;\n\t\t\t\treturn (selfY - paddleY) / halfExtent;\n\t\t\t} else {\n\t\t\t\tconst selfX = selfPosition?.x ?? contactPosition?.x ?? 0;\n\t\t\t\tconst paddleX = otherPosition.x;\n\t\t\t\treturn (selfX - paddleX) / halfExtent;\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: use velocity-based offset\n\t\treturn (velocity.x * tx + velocity.y * ty) / speed;\n\t}\n\n\t/**\n\t * Apply speed constraints to the reflected velocity.\n\t */\n\tprivate applySpeedClamp(\n\t\tvelocity: { x: number; y: number },\n\t\tminSpeed: number,\n\t\tmaxSpeed: number\n\t): { x: number; y: number } {\n\t\tconst currentSpeed = Math.hypot(velocity.x, velocity.y);\n\t\tif (currentSpeed === 0) return velocity;\n\n\t\tconst targetSpeed = clamp(currentSpeed, minSpeed, maxSpeed);\n\t\tconst scale = targetSpeed / currentSpeed;\n\n\t\treturn {\n\t\t\tx: velocity.x * scale,\n\t\t\ty: velocity.y * scale,\n\t\t};\n\t}\n\n\t/**\n\t * Clear the ricochet state (call after consumer has applied the result).\n\t */\n\tclearRicochet(): void {\n\t\tthis.dispatch(Ricochet2DEvent.EndRicochet);\n\t}\n\n\tprivate dispatch(event: Ricochet2DEvent): void {\n\t\tif (this.machine.can(event)) {\n\t\t\tthis.machine.dispatch(event);\n\t\t}\n\t}\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 * Ricochet2DBehavior\n *\n * Computes 2D ricochet/reflection results for entities during collisions.\n * The behavior computes the result; the consumer decides how to apply it.\n *\n * Use `getRicochet(ctx)` on the behavior handle to compute reflection results.\n */\n\nimport type { IWorld } from 'bitecs';\nimport { defineBehavior, type BehaviorRef } from '../behavior-descriptor';\nimport type { BehaviorSystem } from '../behavior-system';\nimport { Ricochet2DFSM, type Ricochet2DResult, type Ricochet2DCollisionContext, type RicochetCallback } from './ricochet-2d-fsm';\nexport type { Ricochet2DResult };\n\nexport interface Ricochet2DOptions {\n\t/**\n\t * Minimum speed after reflection.\n\t * Default: 2\n\t */\n\tminSpeed: number;\n\n\t/**\n\t * Maximum speed after reflection.\n\t * Default: 20\n\t */\n\tmaxSpeed: number;\n\n\t/**\n\t * Speed multiplier applied during angled reflection.\n\t * Default: 1.05\n\t */\n\tspeedMultiplier: number;\n\n\t/**\n\t * Reflection mode:\n\t * - 'simple': Basic axis inversion\n\t * - 'angled': Paddle-style deflection based on contact point\n\t * Default: 'angled'\n\t */\n\treflectionMode: 'simple' | 'angled';\n\n\t/**\n\t * Maximum deflection angle in degrees for angled mode.\n\t * Default: 60\n\t */\n\tmaxAngleDeg: number;\n}\n\n/**\n * Handle methods provided by Ricochet2DBehavior\n */\nexport interface Ricochet2DHandle {\n\t/**\n\t * Compute a ricochet/reflection result from collision context.\n\t * Returns the result for the consumer to apply, or null if invalid input.\n\t *\n\t * @param ctx - Collision context with selfVelocity and contact normal\n\t * @returns Ricochet result with velocity, speed, and normal, or null\n\t */\n\tgetRicochet(ctx: Ricochet2DCollisionContext): Ricochet2DResult | null;\n\n\t/**\n\t * Compute ricochet and apply velocity directly via transformStore.\n\t * Emits to onRicochet listeners if successful.\n\t *\n\t * @param ctx - Collision context with entity and contact normal\n\t * @returns true if ricochet was computed and applied, false otherwise\n\t */\n\tapplyRicochet(ctx: Ricochet2DCollisionContext): boolean;\n\n\t/**\n\t * Get the last computed ricochet result, or null if none.\n\t */\n\tgetLastResult(): Ricochet2DResult | null;\n\n\t/**\n\t * Register a listener for ricochet events.\n\t * Called whenever a ricochet is computed (from getRicochet or applyRicochet).\n\t *\n\t * @param callback - Function to call with ricochet result\n\t * @returns Unsubscribe function\n\t */\n\tonRicochet(callback: RicochetCallback): () => void;\n}\n\nconst defaultOptions: Ricochet2DOptions = {\n\tminSpeed: 2,\n\tmaxSpeed: 20,\n\tspeedMultiplier: 1.05,\n\treflectionMode: 'angled',\n\tmaxAngleDeg: 60,\n};\n\n/**\n * Creates behavior-specific handle methods for Ricochet2DBehavior.\n */\nfunction createRicochet2DHandle(\n\tref: BehaviorRef<Ricochet2DOptions>\n): Ricochet2DHandle {\n\treturn {\n\t\tgetRicochet: (ctx: Ricochet2DCollisionContext) => {\n\t\t\tconst fsm = ref.fsm as Ricochet2DFSM | undefined;\n\t\t\tif (!fsm) return null;\n\t\t\treturn fsm.computeRicochet(ctx, ref.options);\n\t\t},\n\t\tapplyRicochet: (ctx: Ricochet2DCollisionContext): boolean => {\n\t\t\tconst fsm = ref.fsm as Ricochet2DFSM | undefined;\n\t\t\tif (!fsm) return false;\n\n\t\t\t// Skip if on cooldown (prevents rapid duplicate applications)\n\t\t\tif (fsm.isOnCooldown()) return false;\n\n\t\t\tconst result = fsm.computeRicochet(ctx, ref.options);\n\t\t\tif (!result) return false;\n\n\t\t\t// Apply velocity via transformStore\n\t\t\tconst entity = ctx.entity as any;\n\t\t\tif (entity?.transformStore) {\n\t\t\t\tentity.transformStore.velocity.x = result.velocity.x;\n\t\t\t\tentity.transformStore.velocity.y = result.velocity.y;\n\t\t\t\tentity.transformStore.velocity.z = result.velocity.z ?? 0;\n\t\t\t\tentity.transformStore.dirty.velocity = true;\n\t\t\t}\n\n\t\t\treturn true;\n\t\t},\n\t\tgetLastResult: () => {\n\t\t\tconst fsm = ref.fsm as Ricochet2DFSM | undefined;\n\t\t\treturn fsm?.getLastResult() ?? null;\n\t\t},\n\t\tonRicochet: (callback: RicochetCallback): (() => void) => {\n\t\t\tconst fsm = ref.fsm as Ricochet2DFSM | undefined;\n\t\t\tif (!fsm) {\n\t\t\t\t// FSM not ready yet - queue callback for later\n\t\t\t\t// System will apply pending callbacks when FSM is created\n\t\t\t\tif (!(ref as any).pendingListeners) {\n\t\t\t\t\t(ref as any).pendingListeners = [];\n\t\t\t\t}\n\t\t\t\t(ref as any).pendingListeners.push(callback);\n\t\t\t\t\n\t\t\t\t// Return unsubscribe that removes from pending queue\n\t\t\t\treturn () => {\n\t\t\t\t\tconst pending = (ref as any).pendingListeners as RicochetCallback[];\n\t\t\t\t\tconst idx = pending.indexOf(callback);\n\t\t\t\t\tif (idx >= 0) pending.splice(idx, 1);\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn fsm.addListener(callback);\n\t\t},\n\t};\n}\n\n/**\n * Ricochet2DSystem\n *\n * Stage-level system that:\n * - finds entities with this behavior attached\n * - lazily creates FSM instances for each entity\n *\n * Note: This behavior is consumer-driven. The system only manages FSM lifecycle.\n * Consumers call `getRicochet(ctx)` during collision callbacks to compute results.\n */\nclass Ricochet2DSystem implements BehaviorSystem {\n\tprivate elapsedMs: number = 0;\n\n\tconstructor(private world: any) {}\n\n\tupdate(_ecs: IWorld, delta: number): void {\n\t\t// Accumulate elapsed time (delta is in seconds)\n\t\tthis.elapsedMs += delta * 1000;\n\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 ricochetRef = refs.find(\n\t\t\t\t(r: any) => r.descriptor.key === Symbol.for('zylem:behavior:ricochet-2d')\n\t\t\t);\n\n\t\t\tif (!ricochetRef) continue;\n\n\t\t\t// Create FSM lazily on first update after spawn\n\t\t\tif (!ricochetRef.fsm) {\n\t\t\t\tricochetRef.fsm = new Ricochet2DFSM();\n\t\t\t\t\n\t\t\t\t// Apply any pending listeners that were registered before FSM existed\n\t\t\t\tconst pending = (ricochetRef as any).pendingListeners as RicochetCallback[] | undefined;\n\t\t\t\tif (pending) {\n\t\t\t\t\tfor (const cb of pending) {\n\t\t\t\t\t\tricochetRef.fsm.addListener(cb);\n\t\t\t\t\t}\n\t\t\t\t\t(ricochetRef as any).pendingListeners = [];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sync current game time to FSM\n\t\t\tricochetRef.fsm.setCurrentTimeMs(this.elapsedMs);\n\t\t}\n\t}\n\n\tdestroy(_ecs: IWorld): 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\t\t\tif (typeof gameEntity.getBehaviorRefs !== 'function') continue;\n\n\t\t\tconst refs = gameEntity.getBehaviorRefs();\n\t\t\tconst ricochetRef = refs.find(\n\t\t\t\t(r: any) => r.descriptor.key === Symbol.for('zylem:behavior:ricochet-2d')\n\t\t\t);\n\n\t\t\tif (ricochetRef?.fsm) {\n\t\t\t\tricochetRef.fsm.resetCooldown();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Ricochet2DBehavior\n *\n * @example\n * ```ts\n * import { Ricochet2DBehavior } from \"@zylem/game-lib\";\n *\n * const ball = createSphere({ ... });\n * const ricochet = ball.use(Ricochet2DBehavior, {\n * minSpeed: 3,\n * maxSpeed: 15,\n * reflectionMode: 'angled',\n * });\n *\n * ball.onCollision(({ entity, other }) => {\n * const velocity = entity.body.linvel();\n * const result = ricochet.getRicochet({\n * selfVelocity: velocity,\n * contact: { normal: { x: 1, y: 0 } }, // from collision data\n * });\n *\n * if (result) {\n * entity.body.setLinvel(result.velocity, true);\n * }\n * });\n * ```\n */\nexport const Ricochet2DBehavior = defineBehavior({\n\tname: 'ricochet-2d',\n\tdefaultOptions,\n\tsystemFactory: (ctx) => new Ricochet2DSystem(ctx.world),\n\tcreateHandle: createRicochet2DHandle,\n});\n"],"mappings":"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","names":["Ricochet2DState","Ricochet2DEvent"]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { b as BehaviorDescriptor } from '../behavior-descriptor-BWNWmIjv.js';
|
|
2
|
+
import { StateMachine } from 'typescript-fsm';
|
|
3
|
+
import 'bitecs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ScreenWrapBehavior
|
|
7
|
+
*
|
|
8
|
+
* When an entity exits the defined 2D bounds, it wraps around to the opposite edge.
|
|
9
|
+
* Asteroids-style screen wrapping with FSM for edge detection.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Screen wrap options (typed for entity.use() autocomplete)
|
|
13
|
+
*/
|
|
14
|
+
interface ScreenWrapOptions {
|
|
15
|
+
/** Width of the wrapping area (default: 20) */
|
|
16
|
+
width: number;
|
|
17
|
+
/** Height of the wrapping area (default: 15) */
|
|
18
|
+
height: number;
|
|
19
|
+
/** Center X position (default: 0) */
|
|
20
|
+
centerX: number;
|
|
21
|
+
/** Center Y position (default: 0) */
|
|
22
|
+
centerY: number;
|
|
23
|
+
/** Distance from edge to trigger NearEdge state (default: 2) */
|
|
24
|
+
edgeThreshold: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* ScreenWrapBehavior - Wraps entities around 2D bounds
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { ScreenWrapBehavior } from "@zylem/game-lib";
|
|
32
|
+
*
|
|
33
|
+
* const ship = createSprite({ ... });
|
|
34
|
+
* const wrapRef = ship.use(ScreenWrapBehavior, { width: 20, height: 15 });
|
|
35
|
+
*
|
|
36
|
+
* // Access FSM to observe edge state
|
|
37
|
+
* const fsm = wrapRef.getFSM();
|
|
38
|
+
* console.log(fsm?.getState()); // 'center', 'near-edge-left', 'wrapped', etc.
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare const ScreenWrapBehavior: BehaviorDescriptor<ScreenWrapOptions, Record<string, never>, unknown>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* ScreenWrapFSM
|
|
45
|
+
*
|
|
46
|
+
* State machine for screen wrap behavior.
|
|
47
|
+
* Reports position relative to bounds edges.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
declare enum ScreenWrapState {
|
|
51
|
+
Center = "center",
|
|
52
|
+
NearEdgeLeft = "near-edge-left",
|
|
53
|
+
NearEdgeRight = "near-edge-right",
|
|
54
|
+
NearEdgeTop = "near-edge-top",
|
|
55
|
+
NearEdgeBottom = "near-edge-bottom",
|
|
56
|
+
Wrapped = "wrapped"
|
|
57
|
+
}
|
|
58
|
+
declare enum ScreenWrapEvent {
|
|
59
|
+
EnterCenter = "enter-center",
|
|
60
|
+
ApproachLeft = "approach-left",
|
|
61
|
+
ApproachRight = "approach-right",
|
|
62
|
+
ApproachTop = "approach-top",
|
|
63
|
+
ApproachBottom = "approach-bottom",
|
|
64
|
+
Wrap = "wrap"
|
|
65
|
+
}
|
|
66
|
+
declare class ScreenWrapFSM {
|
|
67
|
+
machine: StateMachine<ScreenWrapState, ScreenWrapEvent, never>;
|
|
68
|
+
constructor();
|
|
69
|
+
getState(): ScreenWrapState;
|
|
70
|
+
dispatch(event: ScreenWrapEvent): void;
|
|
71
|
+
/**
|
|
72
|
+
* Update FSM based on entity position relative to bounds
|
|
73
|
+
*/
|
|
74
|
+
update(position: {
|
|
75
|
+
x: number;
|
|
76
|
+
y: number;
|
|
77
|
+
}, bounds: {
|
|
78
|
+
minX: number;
|
|
79
|
+
maxX: number;
|
|
80
|
+
minY: number;
|
|
81
|
+
maxY: number;
|
|
82
|
+
edgeThreshold: number;
|
|
83
|
+
}, wrapped: boolean): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { ScreenWrapBehavior, ScreenWrapEvent, ScreenWrapFSM, type ScreenWrapOptions, ScreenWrapState };
|
|
@@ -0,0 +1,195 @@
|
|
|
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/screen-wrap/screen-wrap-fsm.ts
|
|
12
|
+
import { StateMachine, t } from "typescript-fsm";
|
|
13
|
+
var ScreenWrapState = /* @__PURE__ */ ((ScreenWrapState2) => {
|
|
14
|
+
ScreenWrapState2["Center"] = "center";
|
|
15
|
+
ScreenWrapState2["NearEdgeLeft"] = "near-edge-left";
|
|
16
|
+
ScreenWrapState2["NearEdgeRight"] = "near-edge-right";
|
|
17
|
+
ScreenWrapState2["NearEdgeTop"] = "near-edge-top";
|
|
18
|
+
ScreenWrapState2["NearEdgeBottom"] = "near-edge-bottom";
|
|
19
|
+
ScreenWrapState2["Wrapped"] = "wrapped";
|
|
20
|
+
return ScreenWrapState2;
|
|
21
|
+
})(ScreenWrapState || {});
|
|
22
|
+
var ScreenWrapEvent = /* @__PURE__ */ ((ScreenWrapEvent2) => {
|
|
23
|
+
ScreenWrapEvent2["EnterCenter"] = "enter-center";
|
|
24
|
+
ScreenWrapEvent2["ApproachLeft"] = "approach-left";
|
|
25
|
+
ScreenWrapEvent2["ApproachRight"] = "approach-right";
|
|
26
|
+
ScreenWrapEvent2["ApproachTop"] = "approach-top";
|
|
27
|
+
ScreenWrapEvent2["ApproachBottom"] = "approach-bottom";
|
|
28
|
+
ScreenWrapEvent2["Wrap"] = "wrap";
|
|
29
|
+
return ScreenWrapEvent2;
|
|
30
|
+
})(ScreenWrapEvent || {});
|
|
31
|
+
var ScreenWrapFSM = class {
|
|
32
|
+
machine;
|
|
33
|
+
constructor() {
|
|
34
|
+
this.machine = new StateMachine(
|
|
35
|
+
"center" /* Center */,
|
|
36
|
+
[
|
|
37
|
+
// From Center
|
|
38
|
+
t("center" /* Center */, "approach-left" /* ApproachLeft */, "near-edge-left" /* NearEdgeLeft */),
|
|
39
|
+
t("center" /* Center */, "approach-right" /* ApproachRight */, "near-edge-right" /* NearEdgeRight */),
|
|
40
|
+
t("center" /* Center */, "approach-top" /* ApproachTop */, "near-edge-top" /* NearEdgeTop */),
|
|
41
|
+
t("center" /* Center */, "approach-bottom" /* ApproachBottom */, "near-edge-bottom" /* NearEdgeBottom */),
|
|
42
|
+
// From NearEdge to Wrapped
|
|
43
|
+
t("near-edge-left" /* NearEdgeLeft */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
44
|
+
t("near-edge-right" /* NearEdgeRight */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
45
|
+
t("near-edge-top" /* NearEdgeTop */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
46
|
+
t("near-edge-bottom" /* NearEdgeBottom */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
47
|
+
// From NearEdge back to Center
|
|
48
|
+
t("near-edge-left" /* NearEdgeLeft */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
49
|
+
t("near-edge-right" /* NearEdgeRight */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
50
|
+
t("near-edge-top" /* NearEdgeTop */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
51
|
+
t("near-edge-bottom" /* NearEdgeBottom */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
52
|
+
// From Wrapped back to Center
|
|
53
|
+
t("wrapped" /* Wrapped */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
54
|
+
// From Wrapped to NearEdge (landed near opposite edge)
|
|
55
|
+
t("wrapped" /* Wrapped */, "approach-left" /* ApproachLeft */, "near-edge-left" /* NearEdgeLeft */),
|
|
56
|
+
t("wrapped" /* Wrapped */, "approach-right" /* ApproachRight */, "near-edge-right" /* NearEdgeRight */),
|
|
57
|
+
t("wrapped" /* Wrapped */, "approach-top" /* ApproachTop */, "near-edge-top" /* NearEdgeTop */),
|
|
58
|
+
t("wrapped" /* Wrapped */, "approach-bottom" /* ApproachBottom */, "near-edge-bottom" /* NearEdgeBottom */),
|
|
59
|
+
// Self-transitions (no-ops for redundant events)
|
|
60
|
+
t("center" /* Center */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
61
|
+
t("near-edge-left" /* NearEdgeLeft */, "approach-left" /* ApproachLeft */, "near-edge-left" /* NearEdgeLeft */),
|
|
62
|
+
t("near-edge-right" /* NearEdgeRight */, "approach-right" /* ApproachRight */, "near-edge-right" /* NearEdgeRight */),
|
|
63
|
+
t("near-edge-top" /* NearEdgeTop */, "approach-top" /* ApproachTop */, "near-edge-top" /* NearEdgeTop */),
|
|
64
|
+
t("near-edge-bottom" /* NearEdgeBottom */, "approach-bottom" /* ApproachBottom */, "near-edge-bottom" /* NearEdgeBottom */)
|
|
65
|
+
]
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
getState() {
|
|
69
|
+
return this.machine.getState();
|
|
70
|
+
}
|
|
71
|
+
dispatch(event) {
|
|
72
|
+
if (this.machine.can(event)) {
|
|
73
|
+
this.machine.dispatch(event);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Update FSM based on entity position relative to bounds
|
|
78
|
+
*/
|
|
79
|
+
update(position, bounds, wrapped) {
|
|
80
|
+
const { x, y } = position;
|
|
81
|
+
const { minX, maxX, minY, maxY, edgeThreshold } = bounds;
|
|
82
|
+
if (wrapped) {
|
|
83
|
+
this.dispatch("wrap" /* Wrap */);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const nearLeft = x < minX + edgeThreshold;
|
|
87
|
+
const nearRight = x > maxX - edgeThreshold;
|
|
88
|
+
const nearBottom = y < minY + edgeThreshold;
|
|
89
|
+
const nearTop = y > maxY - edgeThreshold;
|
|
90
|
+
if (nearLeft) {
|
|
91
|
+
this.dispatch("approach-left" /* ApproachLeft */);
|
|
92
|
+
} else if (nearRight) {
|
|
93
|
+
this.dispatch("approach-right" /* ApproachRight */);
|
|
94
|
+
} else if (nearTop) {
|
|
95
|
+
this.dispatch("approach-top" /* ApproachTop */);
|
|
96
|
+
} else if (nearBottom) {
|
|
97
|
+
this.dispatch("approach-bottom" /* ApproachBottom */);
|
|
98
|
+
} else {
|
|
99
|
+
this.dispatch("enter-center" /* EnterCenter */);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/lib/behaviors/screen-wrap/screen-wrap.descriptor.ts
|
|
105
|
+
var defaultOptions = {
|
|
106
|
+
width: 20,
|
|
107
|
+
height: 15,
|
|
108
|
+
centerX: 0,
|
|
109
|
+
centerY: 0,
|
|
110
|
+
edgeThreshold: 2
|
|
111
|
+
};
|
|
112
|
+
var ScreenWrapSystem = class {
|
|
113
|
+
constructor(world) {
|
|
114
|
+
this.world = world;
|
|
115
|
+
}
|
|
116
|
+
update(ecs, delta) {
|
|
117
|
+
if (!this.world?.collisionMap) return;
|
|
118
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
119
|
+
const gameEntity = entity;
|
|
120
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
121
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
122
|
+
const wrapRef = refs.find(
|
|
123
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:screen-wrap")
|
|
124
|
+
);
|
|
125
|
+
if (!wrapRef || !gameEntity.body) continue;
|
|
126
|
+
const options = wrapRef.options;
|
|
127
|
+
if (!wrapRef.fsm) {
|
|
128
|
+
wrapRef.fsm = new ScreenWrapFSM();
|
|
129
|
+
}
|
|
130
|
+
const wrapped = this.wrapEntity(gameEntity, options);
|
|
131
|
+
const pos = gameEntity.body.translation();
|
|
132
|
+
const { width, height, centerX, centerY, edgeThreshold } = options;
|
|
133
|
+
const halfWidth = width / 2;
|
|
134
|
+
const halfHeight = height / 2;
|
|
135
|
+
wrapRef.fsm.update(
|
|
136
|
+
{ x: pos.x, y: pos.y },
|
|
137
|
+
{
|
|
138
|
+
minX: centerX - halfWidth,
|
|
139
|
+
maxX: centerX + halfWidth,
|
|
140
|
+
minY: centerY - halfHeight,
|
|
141
|
+
maxY: centerY + halfHeight,
|
|
142
|
+
edgeThreshold
|
|
143
|
+
},
|
|
144
|
+
wrapped
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
wrapEntity(entity, options) {
|
|
149
|
+
const body = entity.body;
|
|
150
|
+
if (!body) return false;
|
|
151
|
+
const { width, height, centerX, centerY } = options;
|
|
152
|
+
const halfWidth = width / 2;
|
|
153
|
+
const halfHeight = height / 2;
|
|
154
|
+
const minX = centerX - halfWidth;
|
|
155
|
+
const maxX = centerX + halfWidth;
|
|
156
|
+
const minY = centerY - halfHeight;
|
|
157
|
+
const maxY = centerY + halfHeight;
|
|
158
|
+
const pos = body.translation();
|
|
159
|
+
let newX = pos.x;
|
|
160
|
+
let newY = pos.y;
|
|
161
|
+
let wrapped = false;
|
|
162
|
+
if (pos.x < minX) {
|
|
163
|
+
newX = maxX - (minX - pos.x);
|
|
164
|
+
wrapped = true;
|
|
165
|
+
} else if (pos.x > maxX) {
|
|
166
|
+
newX = minX + (pos.x - maxX);
|
|
167
|
+
wrapped = true;
|
|
168
|
+
}
|
|
169
|
+
if (pos.y < minY) {
|
|
170
|
+
newY = maxY - (minY - pos.y);
|
|
171
|
+
wrapped = true;
|
|
172
|
+
} else if (pos.y > maxY) {
|
|
173
|
+
newY = minY + (pos.y - maxY);
|
|
174
|
+
wrapped = true;
|
|
175
|
+
}
|
|
176
|
+
if (wrapped) {
|
|
177
|
+
body.setTranslation({ x: newX, y: newY, z: pos.z }, true);
|
|
178
|
+
}
|
|
179
|
+
return wrapped;
|
|
180
|
+
}
|
|
181
|
+
destroy(_ecs) {
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var ScreenWrapBehavior = defineBehavior({
|
|
185
|
+
name: "screen-wrap",
|
|
186
|
+
defaultOptions,
|
|
187
|
+
systemFactory: (ctx) => new ScreenWrapSystem(ctx.world)
|
|
188
|
+
});
|
|
189
|
+
export {
|
|
190
|
+
ScreenWrapBehavior,
|
|
191
|
+
ScreenWrapEvent,
|
|
192
|
+
ScreenWrapFSM,
|
|
193
|
+
ScreenWrapState
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=screen-wrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/behaviors/behavior-descriptor.ts","../../src/lib/behaviors/screen-wrap/screen-wrap-fsm.ts","../../src/lib/behaviors/screen-wrap/screen-wrap.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 * ScreenWrapFSM\n * \n * State machine for screen wrap behavior.\n * Reports position relative to bounds edges.\n */\n\nimport { StateMachine, t } from 'typescript-fsm';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM State Model\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport enum ScreenWrapState {\n\tCenter = 'center',\n\tNearEdgeLeft = 'near-edge-left',\n\tNearEdgeRight = 'near-edge-right',\n\tNearEdgeTop = 'near-edge-top',\n\tNearEdgeBottom = 'near-edge-bottom',\n\tWrapped = 'wrapped',\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM Events\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport enum ScreenWrapEvent {\n\tEnterCenter = 'enter-center',\n\tApproachLeft = 'approach-left',\n\tApproachRight = 'approach-right',\n\tApproachTop = 'approach-top',\n\tApproachBottom = 'approach-bottom',\n\tWrap = 'wrap',\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ScreenWrapFSM\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class ScreenWrapFSM {\n\tmachine: StateMachine<ScreenWrapState, ScreenWrapEvent, never>;\n\n\tconstructor() {\n\t\tthis.machine = new StateMachine<ScreenWrapState, ScreenWrapEvent, never>(\n\t\t\tScreenWrapState.Center,\n\t\t\t[\n\t\t\t\t// From Center\n\t\t\t\tt(ScreenWrapState.Center, ScreenWrapEvent.ApproachLeft, ScreenWrapState.NearEdgeLeft),\n\t\t\t\tt(ScreenWrapState.Center, ScreenWrapEvent.ApproachRight, ScreenWrapState.NearEdgeRight),\n\t\t\t\tt(ScreenWrapState.Center, ScreenWrapEvent.ApproachTop, ScreenWrapState.NearEdgeTop),\n\t\t\t\tt(ScreenWrapState.Center, ScreenWrapEvent.ApproachBottom, ScreenWrapState.NearEdgeBottom),\n\t\t\t\t\n\t\t\t\t// From NearEdge to Wrapped\n\t\t\t\tt(ScreenWrapState.NearEdgeLeft, ScreenWrapEvent.Wrap, ScreenWrapState.Wrapped),\n\t\t\t\tt(ScreenWrapState.NearEdgeRight, ScreenWrapEvent.Wrap, ScreenWrapState.Wrapped),\n\t\t\t\tt(ScreenWrapState.NearEdgeTop, ScreenWrapEvent.Wrap, ScreenWrapState.Wrapped),\n\t\t\t\tt(ScreenWrapState.NearEdgeBottom, ScreenWrapEvent.Wrap, ScreenWrapState.Wrapped),\n\n\t\t\t\t// From NearEdge back to Center\n\t\t\t\tt(ScreenWrapState.NearEdgeLeft, ScreenWrapEvent.EnterCenter, ScreenWrapState.Center),\n\t\t\t\tt(ScreenWrapState.NearEdgeRight, ScreenWrapEvent.EnterCenter, ScreenWrapState.Center),\n\t\t\t\tt(ScreenWrapState.NearEdgeTop, ScreenWrapEvent.EnterCenter, ScreenWrapState.Center),\n\t\t\t\tt(ScreenWrapState.NearEdgeBottom, ScreenWrapEvent.EnterCenter, ScreenWrapState.Center),\n\n\t\t\t\t// From Wrapped back to Center\n\t\t\t\tt(ScreenWrapState.Wrapped, ScreenWrapEvent.EnterCenter, ScreenWrapState.Center),\n\n\t\t\t\t// From Wrapped to NearEdge (landed near opposite edge)\n\t\t\t\tt(ScreenWrapState.Wrapped, ScreenWrapEvent.ApproachLeft, ScreenWrapState.NearEdgeLeft),\n\t\t\t\tt(ScreenWrapState.Wrapped, ScreenWrapEvent.ApproachRight, ScreenWrapState.NearEdgeRight),\n\t\t\t\tt(ScreenWrapState.Wrapped, ScreenWrapEvent.ApproachTop, ScreenWrapState.NearEdgeTop),\n\t\t\t\tt(ScreenWrapState.Wrapped, ScreenWrapEvent.ApproachBottom, ScreenWrapState.NearEdgeBottom),\n\n\t\t\t\t// Self-transitions (no-ops for redundant events)\n\t\t\t\tt(ScreenWrapState.Center, ScreenWrapEvent.EnterCenter, ScreenWrapState.Center),\n\t\t\t\tt(ScreenWrapState.NearEdgeLeft, ScreenWrapEvent.ApproachLeft, ScreenWrapState.NearEdgeLeft),\n\t\t\t\tt(ScreenWrapState.NearEdgeRight, ScreenWrapEvent.ApproachRight, ScreenWrapState.NearEdgeRight),\n\t\t\t\tt(ScreenWrapState.NearEdgeTop, ScreenWrapEvent.ApproachTop, ScreenWrapState.NearEdgeTop),\n\t\t\t\tt(ScreenWrapState.NearEdgeBottom, ScreenWrapEvent.ApproachBottom, ScreenWrapState.NearEdgeBottom),\n\t\t\t]\n\t\t);\n\t}\n\n\tgetState(): ScreenWrapState {\n\t\treturn this.machine.getState();\n\t}\n\n\tdispatch(event: ScreenWrapEvent): 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 based on entity position relative to bounds\n\t */\n\tupdate(position: { x: number; y: number }, bounds: {\n\t\tminX: number; maxX: number; minY: number; maxY: number;\n\t\tedgeThreshold: number;\n\t}, wrapped: boolean): void {\n\t\tconst { x, y } = position;\n\t\tconst { minX, maxX, minY, maxY, edgeThreshold } = bounds;\n\n\t\tif (wrapped) {\n\t\t\tthis.dispatch(ScreenWrapEvent.Wrap);\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if near edges\n\t\tconst nearLeft = x < minX + edgeThreshold;\n\t\tconst nearRight = x > maxX - edgeThreshold;\n\t\tconst nearBottom = y < minY + edgeThreshold;\n\t\tconst nearTop = y > maxY - edgeThreshold;\n\n\t\tif (nearLeft) {\n\t\t\tthis.dispatch(ScreenWrapEvent.ApproachLeft);\n\t\t} else if (nearRight) {\n\t\t\tthis.dispatch(ScreenWrapEvent.ApproachRight);\n\t\t} else if (nearTop) {\n\t\t\tthis.dispatch(ScreenWrapEvent.ApproachTop);\n\t\t} else if (nearBottom) {\n\t\t\tthis.dispatch(ScreenWrapEvent.ApproachBottom);\n\t\t} else {\n\t\t\tthis.dispatch(ScreenWrapEvent.EnterCenter);\n\t\t}\n\t}\n}\n","/**\n * ScreenWrapBehavior\n * \n * When an entity exits the defined 2D bounds, it wraps around to the opposite edge.\n * Asteroids-style screen wrapping with FSM for edge detection.\n */\n\nimport type { IWorld } from 'bitecs';\nimport { defineBehavior } from '../behavior-descriptor';\nimport type { BehaviorSystem } from '../behavior-system';\nimport { ScreenWrapFSM } from './screen-wrap-fsm';\n\n/**\n * Screen wrap options (typed for entity.use() autocomplete)\n */\nexport interface ScreenWrapOptions {\n\t/** Width of the wrapping area (default: 20) */\n\twidth: number;\n\t/** Height of the wrapping area (default: 15) */\n\theight: number;\n\t/** Center X position (default: 0) */\n\tcenterX: number;\n\t/** Center Y position (default: 0) */\n\tcenterY: number;\n\t/** Distance from edge to trigger NearEdge state (default: 2) */\n\tedgeThreshold: number;\n}\n\nconst defaultOptions: ScreenWrapOptions = {\n\twidth: 20,\n\theight: 15,\n\tcenterX: 0,\n\tcenterY: 0,\n\tedgeThreshold: 2,\n};\n\n/**\n * ScreenWrapSystem - Wraps entities around 2D bounds\n */\nclass ScreenWrapSystem 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\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 wrapRef = refs.find((r: any) => \n\t\t\t\tr.descriptor.key === Symbol.for('zylem:behavior:screen-wrap')\n\t\t\t);\n\t\t\t\n\t\t\tif (!wrapRef || !gameEntity.body) continue;\n\n\t\t\tconst options = wrapRef.options as ScreenWrapOptions;\n\n\t\t\t// Create FSM lazily\n\t\t\tif (!wrapRef.fsm) {\n\t\t\t\twrapRef.fsm = new ScreenWrapFSM();\n\t\t\t}\n\n\t\t\tconst wrapped = this.wrapEntity(gameEntity, options);\n\n\t\t\t// Update FSM with position and wrap state\n\t\t\tconst pos = gameEntity.body.translation();\n\t\t\tconst { width, height, centerX, centerY, edgeThreshold } = options;\n\t\t\tconst halfWidth = width / 2;\n\t\t\tconst halfHeight = height / 2;\n\n\t\t\twrapRef.fsm.update(\n\t\t\t\t{ x: pos.x, y: pos.y },\n\t\t\t\t{\n\t\t\t\t\tminX: centerX - halfWidth,\n\t\t\t\t\tmaxX: centerX + halfWidth,\n\t\t\t\t\tminY: centerY - halfHeight,\n\t\t\t\t\tmaxY: centerY + halfHeight,\n\t\t\t\t\tedgeThreshold,\n\t\t\t\t},\n\t\t\t\twrapped\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate wrapEntity(entity: any, options: ScreenWrapOptions): boolean {\n\t\tconst body = entity.body;\n\t\tif (!body) return false;\n\n\t\tconst { width, height, centerX, centerY } = options;\n\t\tconst halfWidth = width / 2;\n\t\tconst halfHeight = height / 2;\n\n\t\tconst minX = centerX - halfWidth;\n\t\tconst maxX = centerX + halfWidth;\n\t\tconst minY = centerY - halfHeight;\n\t\tconst maxY = centerY + halfHeight;\n\n\t\tconst pos = body.translation();\n\t\tlet newX = pos.x;\n\t\tlet newY = pos.y;\n\t\tlet wrapped = false;\n\n\t\t// Wrap X\n\t\tif (pos.x < minX) {\n\t\t\tnewX = maxX - (minX - pos.x);\n\t\t\twrapped = true;\n\t\t} else if (pos.x > maxX) {\n\t\t\tnewX = minX + (pos.x - maxX);\n\t\t\twrapped = true;\n\t\t}\n\n\t\t// Wrap Y\n\t\tif (pos.y < minY) {\n\t\t\tnewY = maxY - (minY - pos.y);\n\t\t\twrapped = true;\n\t\t} else if (pos.y > maxY) {\n\t\t\tnewY = minY + (pos.y - maxY);\n\t\t\twrapped = true;\n\t\t}\n\n\t\tif (wrapped) {\n\t\t\tbody.setTranslation({ x: newX, y: newY, z: pos.z }, true);\n\t\t}\n\n\t\treturn wrapped;\n\t}\n\n\tdestroy(_ecs: IWorld): void {\n\t\t// Cleanup if needed\n\t}\n}\n\n/**\n * ScreenWrapBehavior - Wraps entities around 2D bounds\n * \n * @example\n * ```typescript\n * import { ScreenWrapBehavior } from \"@zylem/game-lib\";\n * \n * const ship = createSprite({ ... });\n * const wrapRef = ship.use(ScreenWrapBehavior, { width: 20, height: 15 });\n * \n * // Access FSM to observe edge state\n * const fsm = wrapRef.getFSM();\n * console.log(fsm?.getState()); // 'center', 'near-edge-left', 'wrapped', etc.\n * ```\n */\nexport const ScreenWrapBehavior = defineBehavior({\n\tname: 'screen-wrap',\n\tdefaultOptions,\n\tsystemFactory: (ctx) => new ScreenWrapSystem(ctx.world),\n});\n\n"],"mappings":"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","names":["ScreenWrapState","ScreenWrapEvent"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { B as Behavior, m as PlayerInput, p as ThrusterBehavior, q as ThrusterBehaviorOptions, o as ThrusterEntity, j as ThrusterEvent, k as ThrusterFSM, l as ThrusterFSMContext, d as ThrusterInputComponent, n as ThrusterMovementBehavior, b as ThrusterMovementComponent, i as ThrusterState, e as ThrusterStateComponent, g as createThrusterInputComponent, f as createThrusterMovementComponent, h as createThrusterStateComponent } from '../thruster-DhRaJnoL.js';
|
|
2
|
+
import 'typescript-fsm';
|
|
3
|
+
import '../world-Be5m1XC1.js';
|
|
4
|
+
import 'three';
|
|
5
|
+
import '@dimforge/rapier3d-compat';
|
|
6
|
+
import '../entity-Bq_eNEDI.js';
|
|
7
|
+
import '../entity-BpbZqg19.js';
|
|
8
|
+
import 'bitecs';
|
|
9
|
+
import 'mitt';
|
|
10
|
+
import '../behavior-descriptor-BWNWmIjv.js';
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// src/lib/behaviors/thruster/components.ts
|
|
2
|
+
function createThrusterMovementComponent(linearThrust, angularThrust, options) {
|
|
3
|
+
return {
|
|
4
|
+
linearThrust,
|
|
5
|
+
angularThrust,
|
|
6
|
+
linearDamping: options?.linearDamping,
|
|
7
|
+
angularDamping: options?.angularDamping
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function createThrusterInputComponent() {
|
|
11
|
+
return {
|
|
12
|
+
thrust: 0,
|
|
13
|
+
rotate: 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function createThrusterStateComponent() {
|
|
17
|
+
return {
|
|
18
|
+
enabled: true,
|
|
19
|
+
currentThrust: 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/lib/behaviors/thruster/thruster-fsm.ts
|
|
24
|
+
import { StateMachine, t } from "typescript-fsm";
|
|
25
|
+
var ThrusterState = /* @__PURE__ */ ((ThrusterState2) => {
|
|
26
|
+
ThrusterState2["Idle"] = "idle";
|
|
27
|
+
ThrusterState2["Active"] = "active";
|
|
28
|
+
ThrusterState2["Boosting"] = "boosting";
|
|
29
|
+
ThrusterState2["Disabled"] = "disabled";
|
|
30
|
+
ThrusterState2["Docked"] = "docked";
|
|
31
|
+
return ThrusterState2;
|
|
32
|
+
})(ThrusterState || {});
|
|
33
|
+
var ThrusterEvent = /* @__PURE__ */ ((ThrusterEvent2) => {
|
|
34
|
+
ThrusterEvent2["Activate"] = "activate";
|
|
35
|
+
ThrusterEvent2["Deactivate"] = "deactivate";
|
|
36
|
+
ThrusterEvent2["Boost"] = "boost";
|
|
37
|
+
ThrusterEvent2["EndBoost"] = "endBoost";
|
|
38
|
+
ThrusterEvent2["Disable"] = "disable";
|
|
39
|
+
ThrusterEvent2["Enable"] = "enable";
|
|
40
|
+
ThrusterEvent2["Dock"] = "dock";
|
|
41
|
+
ThrusterEvent2["Undock"] = "undock";
|
|
42
|
+
return ThrusterEvent2;
|
|
43
|
+
})(ThrusterEvent || {});
|
|
44
|
+
var ThrusterFSM = class {
|
|
45
|
+
constructor(ctx) {
|
|
46
|
+
this.ctx = ctx;
|
|
47
|
+
this.machine = new StateMachine(
|
|
48
|
+
"idle" /* Idle */,
|
|
49
|
+
[
|
|
50
|
+
// Core transitions
|
|
51
|
+
t("idle" /* Idle */, "activate" /* Activate */, "active" /* Active */),
|
|
52
|
+
t("active" /* Active */, "deactivate" /* Deactivate */, "idle" /* Idle */),
|
|
53
|
+
t("active" /* Active */, "boost" /* Boost */, "boosting" /* Boosting */),
|
|
54
|
+
t("active" /* Active */, "disable" /* Disable */, "disabled" /* Disabled */),
|
|
55
|
+
t("active" /* Active */, "dock" /* Dock */, "docked" /* Docked */),
|
|
56
|
+
t("boosting" /* Boosting */, "endBoost" /* EndBoost */, "active" /* Active */),
|
|
57
|
+
t("boosting" /* Boosting */, "disable" /* Disable */, "disabled" /* Disabled */),
|
|
58
|
+
t("disabled" /* Disabled */, "enable" /* Enable */, "idle" /* Idle */),
|
|
59
|
+
t("docked" /* Docked */, "undock" /* Undock */, "idle" /* Idle */),
|
|
60
|
+
// Self-transitions (no-ops for redundant events)
|
|
61
|
+
t("idle" /* Idle */, "deactivate" /* Deactivate */, "idle" /* Idle */),
|
|
62
|
+
t("active" /* Active */, "activate" /* Activate */, "active" /* Active */)
|
|
63
|
+
]
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
machine;
|
|
67
|
+
/**
|
|
68
|
+
* Get current state
|
|
69
|
+
*/
|
|
70
|
+
getState() {
|
|
71
|
+
return this.machine.getState();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Dispatch an event to transition state
|
|
75
|
+
*/
|
|
76
|
+
dispatch(event) {
|
|
77
|
+
if (this.machine.can(event)) {
|
|
78
|
+
this.machine.dispatch(event);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Update FSM state based on player input.
|
|
83
|
+
* Auto-transitions between Idle/Active to report current state.
|
|
84
|
+
* Does NOT modify input - just observes and reports.
|
|
85
|
+
*/
|
|
86
|
+
update(playerInput) {
|
|
87
|
+
const state = this.machine.getState();
|
|
88
|
+
const hasInput = Math.abs(playerInput.thrust) > 0.01 || Math.abs(playerInput.rotate) > 0.01;
|
|
89
|
+
if (hasInput && state === "idle" /* Idle */) {
|
|
90
|
+
this.dispatch("activate" /* Activate */);
|
|
91
|
+
} else if (!hasInput && state === "active" /* Active */) {
|
|
92
|
+
this.dispatch("deactivate" /* Deactivate */);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/lib/behaviors/thruster/thruster-movement.behavior.ts
|
|
98
|
+
var ThrusterMovementBehavior = class {
|
|
99
|
+
constructor(world) {
|
|
100
|
+
this.world = world;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Query function - returns entities with required thruster components
|
|
104
|
+
*/
|
|
105
|
+
queryEntities() {
|
|
106
|
+
const entities = [];
|
|
107
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
108
|
+
const gameEntity = entity;
|
|
109
|
+
if (gameEntity.physics?.body && gameEntity.thruster && gameEntity.$thruster) {
|
|
110
|
+
entities.push({
|
|
111
|
+
physics: gameEntity.physics,
|
|
112
|
+
thruster: gameEntity.thruster,
|
|
113
|
+
$thruster: gameEntity.$thruster
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return entities;
|
|
118
|
+
}
|
|
119
|
+
update(_dt) {
|
|
120
|
+
const entities = this.queryEntities();
|
|
121
|
+
for (const e of entities) {
|
|
122
|
+
const body = e.physics.body;
|
|
123
|
+
const thruster = e.thruster;
|
|
124
|
+
const input = e.$thruster;
|
|
125
|
+
const q = body.rotation();
|
|
126
|
+
const rotationZ = Math.atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z));
|
|
127
|
+
if (input.thrust !== 0) {
|
|
128
|
+
const currentVel = body.linvel();
|
|
129
|
+
if (input.thrust > 0) {
|
|
130
|
+
const forwardX = Math.sin(-rotationZ);
|
|
131
|
+
const forwardY = Math.cos(-rotationZ);
|
|
132
|
+
const thrustAmount = thruster.linearThrust * input.thrust * 0.1;
|
|
133
|
+
body.setLinvel({
|
|
134
|
+
x: currentVel.x + forwardX * thrustAmount,
|
|
135
|
+
y: currentVel.y + forwardY * thrustAmount,
|
|
136
|
+
z: currentVel.z
|
|
137
|
+
}, true);
|
|
138
|
+
} else {
|
|
139
|
+
const brakeAmount = 0.9;
|
|
140
|
+
body.setLinvel({
|
|
141
|
+
x: currentVel.x * brakeAmount,
|
|
142
|
+
y: currentVel.y * brakeAmount,
|
|
143
|
+
z: currentVel.z
|
|
144
|
+
}, true);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (input.rotate !== 0) {
|
|
148
|
+
body.setAngvel({ x: 0, y: 0, z: -thruster.angularThrust * input.rotate }, true);
|
|
149
|
+
} else {
|
|
150
|
+
const angVel = body.angvel();
|
|
151
|
+
body.setAngvel({ x: angVel.x, y: angVel.y, z: 0 }, true);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/lib/behaviors/behavior-descriptor.ts
|
|
158
|
+
function defineBehavior(config) {
|
|
159
|
+
return {
|
|
160
|
+
key: /* @__PURE__ */ Symbol.for(`zylem:behavior:${config.name}`),
|
|
161
|
+
defaultOptions: config.defaultOptions,
|
|
162
|
+
systemFactory: config.systemFactory,
|
|
163
|
+
createHandle: config.createHandle
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/lib/behaviors/thruster/thruster.descriptor.ts
|
|
168
|
+
var defaultOptions = {
|
|
169
|
+
linearThrust: 10,
|
|
170
|
+
angularThrust: 5
|
|
171
|
+
};
|
|
172
|
+
var ThrusterBehaviorSystem = class {
|
|
173
|
+
constructor(world) {
|
|
174
|
+
this.world = world;
|
|
175
|
+
this.movementBehavior = new ThrusterMovementBehavior(world);
|
|
176
|
+
}
|
|
177
|
+
movementBehavior;
|
|
178
|
+
update(ecs, delta) {
|
|
179
|
+
if (!this.world?.collisionMap) return;
|
|
180
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
181
|
+
const gameEntity = entity;
|
|
182
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
183
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
184
|
+
const thrusterRef = refs.find(
|
|
185
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:thruster")
|
|
186
|
+
);
|
|
187
|
+
if (!thrusterRef || !gameEntity.body) continue;
|
|
188
|
+
const options = thrusterRef.options;
|
|
189
|
+
if (!gameEntity.thruster) {
|
|
190
|
+
gameEntity.thruster = {
|
|
191
|
+
linearThrust: options.linearThrust,
|
|
192
|
+
angularThrust: options.angularThrust
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (!gameEntity.$thruster) {
|
|
196
|
+
gameEntity.$thruster = {
|
|
197
|
+
thrust: 0,
|
|
198
|
+
rotate: 0
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (!gameEntity.physics) {
|
|
202
|
+
gameEntity.physics = { body: gameEntity.body };
|
|
203
|
+
}
|
|
204
|
+
if (!thrusterRef.fsm && gameEntity.$thruster) {
|
|
205
|
+
thrusterRef.fsm = new ThrusterFSM({ input: gameEntity.$thruster });
|
|
206
|
+
}
|
|
207
|
+
if (thrusterRef.fsm && gameEntity.$thruster) {
|
|
208
|
+
thrusterRef.fsm.update({
|
|
209
|
+
thrust: gameEntity.$thruster.thrust,
|
|
210
|
+
rotate: gameEntity.$thruster.rotate
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.movementBehavior.update(delta);
|
|
215
|
+
}
|
|
216
|
+
destroy(_ecs) {
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var ThrusterBehavior = defineBehavior({
|
|
220
|
+
name: "thruster",
|
|
221
|
+
defaultOptions,
|
|
222
|
+
systemFactory: (ctx) => new ThrusterBehaviorSystem(ctx.world)
|
|
223
|
+
});
|
|
224
|
+
export {
|
|
225
|
+
ThrusterBehavior,
|
|
226
|
+
ThrusterEvent,
|
|
227
|
+
ThrusterFSM,
|
|
228
|
+
ThrusterMovementBehavior,
|
|
229
|
+
ThrusterState,
|
|
230
|
+
createThrusterInputComponent,
|
|
231
|
+
createThrusterMovementComponent,
|
|
232
|
+
createThrusterStateComponent
|
|
233
|
+
};
|
|
234
|
+
//# sourceMappingURL=thruster.js.map
|