ecspresso 0.13.0 → 0.13.1

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.
Files changed (79) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/plugins/ai/detection.d.ts +118 -0
  4. package/dist/plugins/ai/detection.js +4 -0
  5. package/dist/plugins/ai/detection.js.map +10 -0
  6. package/dist/plugins/{audio.js → audio/audio.js} +1 -1
  7. package/dist/plugins/{audio.js.map → audio/audio.js.map} +2 -2
  8. package/dist/plugins/combat/health.d.ts +98 -0
  9. package/dist/plugins/combat/health.js +4 -0
  10. package/dist/plugins/combat/health.js.map +10 -0
  11. package/dist/plugins/combat/projectile.d.ts +115 -0
  12. package/dist/plugins/combat/projectile.js +4 -0
  13. package/dist/plugins/combat/projectile.js.map +10 -0
  14. package/dist/plugins/{diagnostics.js → debug/diagnostics.js} +1 -1
  15. package/dist/plugins/{diagnostics.js.map → debug/diagnostics.js.map} +2 -2
  16. package/dist/plugins/{input.js → input/input.js} +1 -1
  17. package/dist/plugins/{input.js.map → input/input.js.map} +2 -2
  18. package/dist/plugins/input/selection.d.ts +114 -0
  19. package/dist/plugins/input/selection.js +4 -0
  20. package/dist/plugins/input/selection.js.map +11 -0
  21. package/dist/plugins/isometric/depth-sort.d.ts +44 -0
  22. package/dist/plugins/isometric/depth-sort.js +4 -0
  23. package/dist/plugins/isometric/depth-sort.js.map +10 -0
  24. package/dist/plugins/isometric/projection.d.ts +83 -0
  25. package/dist/plugins/isometric/projection.js +4 -0
  26. package/dist/plugins/isometric/projection.js.map +10 -0
  27. package/dist/plugins/{collision.d.ts → physics/collision.d.ts} +1 -1
  28. package/dist/plugins/{collision.js → physics/collision.js} +1 -1
  29. package/dist/plugins/{collision.js.map → physics/collision.js.map} +3 -3
  30. package/dist/plugins/{physics2D.d.ts → physics/physics2D.d.ts} +1 -1
  31. package/dist/plugins/{physics2D.js → physics/physics2D.js} +1 -1
  32. package/dist/plugins/{physics2D.js.map → physics/physics2D.js.map} +3 -3
  33. package/dist/plugins/physics/steering.d.ts +102 -0
  34. package/dist/plugins/physics/steering.js +4 -0
  35. package/dist/plugins/physics/steering.js.map +10 -0
  36. package/dist/plugins/{particles.d.ts → rendering/particles.d.ts} +2 -2
  37. package/dist/plugins/{particles.js → rendering/particles.js} +1 -1
  38. package/dist/plugins/rendering/particles.js.map +10 -0
  39. package/dist/plugins/{renderers → rendering}/renderer2D.d.ts +9 -5
  40. package/dist/plugins/rendering/renderer2D.js +4 -0
  41. package/dist/plugins/rendering/renderer2D.js.map +10 -0
  42. package/dist/plugins/{sprite-animation.js → rendering/sprite-animation.js} +1 -1
  43. package/dist/plugins/{sprite-animation.js.map → rendering/sprite-animation.js.map} +2 -2
  44. package/dist/plugins/{coroutine.js → scripting/coroutine.js} +1 -1
  45. package/dist/plugins/{coroutine.js.map → scripting/coroutine.js.map} +2 -2
  46. package/dist/plugins/{state-machine.js → scripting/state-machine.js} +1 -1
  47. package/dist/plugins/{state-machine.js.map → scripting/state-machine.js.map} +2 -2
  48. package/dist/plugins/{timers.js → scripting/timers.js} +1 -1
  49. package/dist/plugins/{timers.js.map → scripting/timers.js.map} +2 -2
  50. package/dist/plugins/{tween.d.ts → scripting/tween.d.ts} +1 -1
  51. package/dist/plugins/{tween.js → scripting/tween.js} +1 -1
  52. package/dist/plugins/scripting/tween.js.map +11 -0
  53. package/dist/plugins/{bounds.js → spatial/bounds.js} +1 -1
  54. package/dist/plugins/{bounds.js.map → spatial/bounds.js.map} +2 -2
  55. package/dist/plugins/{camera.d.ts → spatial/camera.d.ts} +43 -12
  56. package/dist/plugins/spatial/camera.js +4 -0
  57. package/dist/plugins/spatial/camera.js.map +10 -0
  58. package/dist/plugins/{spatial-index.d.ts → spatial/spatial-index.d.ts} +2 -2
  59. package/dist/plugins/{spatial-index.js → spatial/spatial-index.js} +1 -1
  60. package/dist/plugins/{spatial-index.js.map → spatial/spatial-index.js.map} +3 -3
  61. package/dist/plugins/{transform.d.ts → spatial/transform.d.ts} +1 -1
  62. package/dist/plugins/{transform.js → spatial/transform.js} +1 -1
  63. package/dist/plugins/spatial/transform.js.map +10 -0
  64. package/package.json +77 -49
  65. package/dist/plugins/camera.js +0 -4
  66. package/dist/plugins/camera.js.map +0 -10
  67. package/dist/plugins/particles.js.map +0 -10
  68. package/dist/plugins/renderers/renderer2D.js +0 -4
  69. package/dist/plugins/renderers/renderer2D.js.map +0 -10
  70. package/dist/plugins/transform.js.map +0 -10
  71. package/dist/plugins/tween.js.map +0 -11
  72. /package/dist/plugins/{audio.d.ts → audio/audio.d.ts} +0 -0
  73. /package/dist/plugins/{diagnostics.d.ts → debug/diagnostics.d.ts} +0 -0
  74. /package/dist/plugins/{input.d.ts → input/input.d.ts} +0 -0
  75. /package/dist/plugins/{sprite-animation.d.ts → rendering/sprite-animation.d.ts} +0 -0
  76. /package/dist/plugins/{coroutine.d.ts → scripting/coroutine.d.ts} +0 -0
  77. /package/dist/plugins/{state-machine.d.ts → scripting/state-machine.d.ts} +0 -0
  78. /package/dist/plugins/{timers.d.ts → scripting/timers.d.ts} +0 -0
  79. /package/dist/plugins/{bounds.d.ts → spatial/bounds.d.ts} +0 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Health Plugin for ECSpresso
3
+ *
4
+ * Provides a standard health/damage/death lifecycle.
5
+ * Entities with a `health` component can receive `damage` events.
6
+ * When health reaches zero, an `entityDied` event is published.
7
+ * The plugin does NOT remove dead entities — game-specific logic
8
+ * decides when and how to handle death (animations, loot, etc).
9
+ */
10
+ import { type BasePluginOptions } from 'ecspresso';
11
+ import type { WorldConfigFrom } from 'ecspresso';
12
+ /**
13
+ * Health state for an entity.
14
+ */
15
+ export interface Health {
16
+ current: number;
17
+ max: number;
18
+ }
19
+ /**
20
+ * Component types provided by the health plugin.
21
+ */
22
+ export interface HealthComponentTypes {
23
+ health: Health;
24
+ }
25
+ /**
26
+ * Event requesting damage to an entity.
27
+ */
28
+ export interface DamageEvent {
29
+ entityId: number;
30
+ amount: number;
31
+ sourceId?: number;
32
+ }
33
+ /**
34
+ * Event fired when an entity's health reaches zero.
35
+ */
36
+ export interface EntityDiedEvent {
37
+ entityId: number;
38
+ killerId?: number;
39
+ }
40
+ /**
41
+ * Event types provided by the health plugin.
42
+ */
43
+ export interface HealthEventTypes {
44
+ damage: DamageEvent;
45
+ entityDied: EntityDiedEvent;
46
+ }
47
+ /**
48
+ * WorldConfig representing the health plugin's provided types.
49
+ * Used as the `Requires` type parameter by plugins that depend on health.
50
+ */
51
+ export type HealthWorldConfig = WorldConfigFrom<HealthComponentTypes, HealthEventTypes>;
52
+ export interface HealthPluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {
53
+ }
54
+ /**
55
+ * Create a health component at full HP.
56
+ *
57
+ * @param max Maximum (and initial) health
58
+ * @returns Component object suitable for spreading into spawn()
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * ecs.spawn({
63
+ * ...createHealth(100),
64
+ * ...createLocalTransform(200, 300),
65
+ * });
66
+ * ```
67
+ */
68
+ export declare function createHealth(max: number): Pick<HealthComponentTypes, 'health'>;
69
+ /**
70
+ * Create a health component with a specific current value.
71
+ *
72
+ * @param current Current health
73
+ * @param max Maximum health
74
+ * @returns Component object suitable for spreading into spawn()
75
+ */
76
+ export declare function createHealthWith(current: number, max: number): Pick<HealthComponentTypes, 'health'>;
77
+ /**
78
+ * Create a health plugin for ECSpresso.
79
+ *
80
+ * Provides event-driven damage processing. Subscribe to `damage` events
81
+ * to deal damage, and listen to `entityDied` events to react to deaths.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const ecs = ECSpresso.create()
86
+ * .withPlugin(createHealthPlugin())
87
+ * .build();
88
+ *
89
+ * // Deal damage:
90
+ * ecs.eventBus.publish('damage', { entityId: targetId, amount: 25 });
91
+ *
92
+ * // React to death:
93
+ * ecs.on('entityDied', ({ entityId }) => {
94
+ * ecs.commands.removeEntity(entityId);
95
+ * });
96
+ * ```
97
+ */
98
+ export declare function createHealthPlugin<G extends string = 'combat'>(options?: HealthPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, HealthComponentTypes>, HealthEventTypes>, import("ecspresso").EmptyConfig, "health-damage", G, never, never>;
@@ -0,0 +1,4 @@
1
+ var K=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,A)=>(typeof require<"u"?require:k)[A]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as J}from"ecspresso";function N(j){return{health:{current:j,max:j}}}function O(j,k){return{health:{current:j,max:k}}}function Q(j){let{systemGroup:k="combat"}=j??{};return J("health").withComponentTypes().withEventTypes().withLabels().withGroups().install((A)=>{A.addSystem("health-damage").inGroup(k).setEventHandlers({damage({data:q,ecs:F}){let z=F.getComponent(q.entityId,"health");if(!z)return;if(z.current<=0)return;if(z.current=Math.max(0,z.current-q.amount),F.markChanged(q.entityId,"health"),z.current<=0)F.eventBus.publish("entityDied",{entityId:q.entityId,killerId:q.sourceId})}})})}export{O as createHealthWith,Q as createHealthPlugin,N as createHealth};
2
+
3
+ //# debugId=F64127187EB3461D64756E2164756E21
4
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plugins/combat/health.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Health Plugin for ECSpresso\n *\n * Provides a standard health/damage/death lifecycle.\n * Entities with a `health` component can receive `damage` events.\n * When health reaches zero, an `entityDied` event is published.\n * The plugin does NOT remove dead entities — game-specific logic\n * decides when and how to handle death (animations, loot, etc).\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\n\n// ==================== Component Types ====================\n\n/**\n * Health state for an entity.\n */\nexport interface Health {\n\tcurrent: number;\n\tmax: number;\n}\n\n/**\n * Component types provided by the health plugin.\n */\nexport interface HealthComponentTypes {\n\thealth: Health;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event requesting damage to an entity.\n */\nexport interface DamageEvent {\n\tentityId: number;\n\tamount: number;\n\tsourceId?: number;\n}\n\n/**\n * Event fired when an entity's health reaches zero.\n */\nexport interface EntityDiedEvent {\n\tentityId: number;\n\tkillerId?: number;\n}\n\n/**\n * Event types provided by the health plugin.\n */\nexport interface HealthEventTypes {\n\tdamage: DamageEvent;\n\tentityDied: EntityDiedEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the health plugin's provided types.\n * Used as the `Requires` type parameter by plugins that depend on health.\n */\nexport type HealthWorldConfig = WorldConfigFrom<HealthComponentTypes, HealthEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface HealthPluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a health component at full HP.\n *\n * @param max Maximum (and initial) health\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createHealth(100),\n * ...createLocalTransform(200, 300),\n * });\n * ```\n */\nexport function createHealth(max: number): Pick<HealthComponentTypes, 'health'> {\n\treturn { health: { current: max, max } };\n}\n\n/**\n * Create a health component with a specific current value.\n *\n * @param current Current health\n * @param max Maximum health\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createHealthWith(current: number, max: number): Pick<HealthComponentTypes, 'health'> {\n\treturn { health: { current, max } };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a health plugin for ECSpresso.\n *\n * Provides event-driven damage processing. Subscribe to `damage` events\n * to deal damage, and listen to `entityDied` events to react to deaths.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createHealthPlugin())\n * .build();\n *\n * // Deal damage:\n * ecs.eventBus.publish('damage', { entityId: targetId, amount: 25 });\n *\n * // React to death:\n * ecs.on('entityDied', ({ entityId }) => {\n * ecs.commands.removeEntity(entityId);\n * });\n * ```\n */\nexport function createHealthPlugin<G extends string = 'combat'>(\n\toptions?: HealthPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'combat',\n\t} = options ?? {};\n\n\treturn definePlugin('health')\n\t\t.withComponentTypes<HealthComponentTypes>()\n\t\t.withEventTypes<HealthEventTypes>()\n\t\t.withLabels<'health-damage'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('health-damage')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tdamage({ data, ecs }) {\n\t\t\t\t\t\tconst health = ecs.getComponent(data.entityId, 'health');\n\t\t\t\t\t\tif (!health) return;\n\t\t\t\t\t\tif (health.current <= 0) return;\n\n\t\t\t\t\t\thealth.current = Math.max(0, health.current - data.amount);\n\t\t\t\t\t\tecs.markChanged(data.entityId, 'health');\n\n\t\t\t\t\t\tif (health.current <= 0) {\n\t\t\t\t\t\t\tecs.eventBus.publish('entityDied', {\n\t\t\t\t\t\t\t\tentityId: data.entityId,\n\t\t\t\t\t\t\t\tkillerId: data.sourceId,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n"
6
+ ],
7
+ "mappings": "2PAUA,uBAAS,kBA2EF,SAAS,CAAY,CAAC,EAAmD,CAC/E,MAAO,CAAE,OAAQ,CAAE,QAAS,EAAK,KAAI,CAAE,EAUjC,SAAS,CAAgB,CAAC,EAAiB,EAAmD,CACpG,MAAO,CAAE,OAAQ,CAAE,UAAS,KAAI,CAAE,EA0B5B,SAAS,CAA+C,CAC9D,EACC,CACD,IACC,cAAc,UACX,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAAyC,EACzC,eAAiC,EACjC,WAA4B,EAC5B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,eAAe,EACzB,QAAQ,CAAW,EACnB,iBAAiB,CACjB,MAAM,EAAG,OAAM,OAAO,CACrB,IAAM,EAAS,EAAI,aAAa,EAAK,SAAU,QAAQ,EACvD,GAAI,CAAC,EAAQ,OACb,GAAI,EAAO,SAAW,EAAG,OAKzB,GAHA,EAAO,QAAU,KAAK,IAAI,EAAG,EAAO,QAAU,EAAK,MAAM,EACzD,EAAI,YAAY,EAAK,SAAU,QAAQ,EAEnC,EAAO,SAAW,EACrB,EAAI,SAAS,QAAQ,aAAc,CAClC,SAAU,EAAK,SACf,SAAU,EAAK,QAChB,CAAC,EAGJ,CAAC,EACF",
8
+ "debugId": "F64127187EB3461D64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Projectile Plugin for ECSpresso
3
+ *
4
+ * Provides projectile movement (homing and linear) and collision integration.
5
+ * Homing projectiles track a target entity's position each frame.
6
+ * Linear projectiles move in a fixed direction.
7
+ * When a collision involves a projectile, a `projectileHit` event is published
8
+ * and a `damage` event is forwarded to the target (if the health plugin is present).
9
+ */
10
+ import { type BasePluginOptions } from 'ecspresso';
11
+ import type { WorldConfigFrom } from 'ecspresso';
12
+ import type { TransformWorldConfig } from '../spatial/transform';
13
+ import type { CollisionEventTypes } from '../physics/collision';
14
+ import type { DamageEvent } from './health';
15
+ /**
16
+ * Core projectile data.
17
+ */
18
+ export interface Projectile {
19
+ damage: number;
20
+ speed: number;
21
+ /** Entity that fired this projectile */
22
+ sourceId: number;
23
+ }
24
+ /**
25
+ * Homing target — projectile tracks this entity's position each frame.
26
+ */
27
+ export interface ProjectileTarget {
28
+ entityId: number;
29
+ }
30
+ /**
31
+ * Fixed direction for non-homing projectiles (normalized).
32
+ */
33
+ export interface ProjectileDirection {
34
+ x: number;
35
+ y: number;
36
+ }
37
+ /**
38
+ * Component types provided by the projectile plugin.
39
+ */
40
+ export interface ProjectileComponentTypes {
41
+ projectile: Projectile;
42
+ projectileTarget: ProjectileTarget;
43
+ projectileDirection: ProjectileDirection;
44
+ }
45
+ /**
46
+ * Event fired when a projectile hits a target via collision.
47
+ */
48
+ export interface ProjectileHitEvent {
49
+ projectileId: number;
50
+ targetId: number;
51
+ damage: number;
52
+ }
53
+ /**
54
+ * Event types provided by the projectile plugin.
55
+ */
56
+ export interface ProjectileEventTypes {
57
+ projectileHit: ProjectileHitEvent;
58
+ damage: DamageEvent;
59
+ }
60
+ /**
61
+ * WorldConfig representing the projectile plugin's provided types.
62
+ */
63
+ export type ProjectileWorldConfig = WorldConfigFrom<ProjectileComponentTypes, ProjectileEventTypes>;
64
+ export interface ProjectilePluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {
65
+ /**
66
+ * Whether to auto-publish `damage` events on hit.
67
+ * Requires the health plugin to be installed. (default: true)
68
+ */
69
+ publishDamage?: boolean;
70
+ }
71
+ /**
72
+ * Create a projectile component.
73
+ *
74
+ * @param damage Damage dealt on hit
75
+ * @param speed Movement speed in pixels per second
76
+ * @param sourceId Entity that fired this projectile
77
+ * @returns Component object suitable for spreading into spawn()
78
+ */
79
+ export declare function createProjectile(damage: number, speed: number, sourceId: number): Pick<ProjectileComponentTypes, 'projectile'>;
80
+ /**
81
+ * Create a homing projectile target component.
82
+ *
83
+ * @param entityId Target entity to track
84
+ * @returns Component object suitable for spreading into spawn()
85
+ */
86
+ export declare function createProjectileTarget(entityId: number): Pick<ProjectileComponentTypes, 'projectileTarget'>;
87
+ /**
88
+ * Create a fixed-direction projectile component (auto-normalizes).
89
+ *
90
+ * @param x Direction x
91
+ * @param y Direction y
92
+ * @returns Component object suitable for spreading into spawn()
93
+ */
94
+ export declare function createProjectileDirection(x: number, y: number): Pick<ProjectileComponentTypes, 'projectileDirection'>;
95
+ /**
96
+ * Create a projectile plugin for ECSpresso.
97
+ *
98
+ * Provides homing and linear projectile movement systems, plus
99
+ * automatic collision-to-damage integration.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Spawn a homing projectile:
104
+ * ecs.spawn({
105
+ * ...createProjectile(10, 400, turretId),
106
+ * ...createProjectileTarget(enemyId),
107
+ * ...createLocalTransform(x, y),
108
+ * ...createCircleCollider(4),
109
+ * ...collisionLayers.turretProjectile(),
110
+ * sprite: bulletSprite,
111
+ * renderLayer: 'projectiles',
112
+ * });
113
+ * ```
114
+ */
115
+ export declare function createProjectilePlugin<G extends string = 'combat'>(options?: ProjectilePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, ProjectileComponentTypes>, ProjectileEventTypes>, TransformWorldConfig & WorldConfigFrom<{}, CollisionEventTypes<string>>, "projectile-homing" | "projectile-linear" | "projectile-collision", G, never, never>;
@@ -0,0 +1,4 @@
1
+ var Z=((v)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(v,{get:(E,H)=>(typeof require<"u"?require:E)[H]}):v)(function(v){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+v+'" is not supported')});import{definePlugin as Y}from"ecspresso";function b(v,E,H){return{projectile:{damage:v,speed:E,sourceId:H}}}function B(v){return{projectileTarget:{entityId:v}}}function S(v,E){let H=Math.sqrt(v*v+E*E);if(H===0)return{projectileDirection:{x:0,y:-1}};return{projectileDirection:{x:v/H,y:E/H}}}function G(v){let{systemGroup:E="combat",priority:H=300,phase:U="update",publishDamage:X=!0}=v??{};return Y("projectile").withComponentTypes().withEventTypes().withLabels().withGroups().requires().install((R)=>{R.addSystem("projectile-homing").setPriority(H).inPhase(U).inGroup(E).addQuery("homing",{with:["projectile","projectileTarget","localTransform"]}).setProcess(({queries:C,ecs:k,dt:M})=>{for(let K of C.homing){let{projectile:L,projectileTarget:F,localTransform:z}=K.components;if(!k.getEntity(F.entityId)){k.commands.removeEntity(K.id);continue}let J=k.getComponent(F.entityId,"worldTransform");if(!J){k.commands.removeEntity(K.id);continue}let N=J.x-z.x,O=J.y-z.y,V=N*N+O*O,Q=L.speed*M;if(V<=Q*Q)z.x=J.x,z.y=J.y;else{let W=Math.sqrt(V);z.x+=N/W*Q,z.y+=O/W*Q,z.rotation=Math.atan2(O,N)}k.markChanged(K.id,"localTransform")}}),R.addSystem("projectile-linear").setPriority(H).inPhase(U).inGroup(E).addQuery("linear",{with:["projectile","projectileDirection","localTransform"]}).setProcess(({queries:C,dt:k})=>{for(let M of C.linear){let{projectile:K,projectileDirection:L,localTransform:F}=M.components,z=K.speed*k;F.x+=L.x*z,F.y+=L.y*z}}),R.addSystem("projectile-collision").inGroup(E).setEventHandlers({collision({data:C,ecs:k}){if(!k.getEntity(C.entityA)||!k.getEntity(C.entityB))return;let M=k.getComponent(C.entityA,"projectile"),K=k.getComponent(C.entityB,"projectile"),L=M!==void 0,F=L?M:K;if(!F)return;let z=L?C.entityA:C.entityB,J=L?C.entityB:C.entityA;if(J===F.sourceId)return;if(k.eventBus.publish("projectileHit",{projectileId:z,targetId:J,damage:F.damage}),X)k.eventBus.publish("damage",{entityId:J,amount:F.damage,sourceId:F.sourceId});k.commands.removeEntity(z)}})})}export{B as createProjectileTarget,G as createProjectilePlugin,S as createProjectileDirection,b as createProjectile};
2
+
3
+ //# debugId=D0756C0A27AA8CAC64756E2164756E21
4
+ //# sourceMappingURL=projectile.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/plugins/combat/projectile.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Projectile Plugin for ECSpresso\n *\n * Provides projectile movement (homing and linear) and collision integration.\n * Homing projectiles track a target entity's position each frame.\n * Linear projectiles move in a fixed direction.\n * When a collision involves a projectile, a `projectileHit` event is published\n * and a `damage` event is forwarded to the target (if the health plugin is present).\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { CollisionEventTypes } from '../physics/collision';\nimport type { DamageEvent } from './health';\n\n// ==================== Component Types ====================\n\n/**\n * Core projectile data.\n */\nexport interface Projectile {\n\tdamage: number;\n\tspeed: number;\n\t/** Entity that fired this projectile */\n\tsourceId: number;\n}\n\n/**\n * Homing target — projectile tracks this entity's position each frame.\n */\nexport interface ProjectileTarget {\n\tentityId: number;\n}\n\n/**\n * Fixed direction for non-homing projectiles (normalized).\n */\nexport interface ProjectileDirection {\n\tx: number;\n\ty: number;\n}\n\n/**\n * Component types provided by the projectile plugin.\n */\nexport interface ProjectileComponentTypes {\n\tprojectile: Projectile;\n\tprojectileTarget: ProjectileTarget;\n\tprojectileDirection: ProjectileDirection;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when a projectile hits a target via collision.\n */\nexport interface ProjectileHitEvent {\n\tprojectileId: number;\n\ttargetId: number;\n\tdamage: number;\n}\n\n/**\n * Event types provided by the projectile plugin.\n */\nexport interface ProjectileEventTypes {\n\tprojectileHit: ProjectileHitEvent;\n\tdamage: DamageEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the projectile plugin's provided types.\n */\nexport type ProjectileWorldConfig = WorldConfigFrom<ProjectileComponentTypes, ProjectileEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface ProjectilePluginOptions<G extends string = 'combat'> extends BasePluginOptions<G> {\n\t/**\n\t * Whether to auto-publish `damage` events on hit.\n\t * Requires the health plugin to be installed. (default: true)\n\t */\n\tpublishDamage?: boolean;\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a projectile component.\n *\n * @param damage Damage dealt on hit\n * @param speed Movement speed in pixels per second\n * @param sourceId Entity that fired this projectile\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createProjectile(\n\tdamage: number,\n\tspeed: number,\n\tsourceId: number,\n): Pick<ProjectileComponentTypes, 'projectile'> {\n\treturn { projectile: { damage, speed, sourceId } };\n}\n\n/**\n * Create a homing projectile target component.\n *\n * @param entityId Target entity to track\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createProjectileTarget(entityId: number): Pick<ProjectileComponentTypes, 'projectileTarget'> {\n\treturn { projectileTarget: { entityId } };\n}\n\n/**\n * Create a fixed-direction projectile component (auto-normalizes).\n *\n * @param x Direction x\n * @param y Direction y\n * @returns Component object suitable for spreading into spawn()\n */\nexport function createProjectileDirection(x: number, y: number): Pick<ProjectileComponentTypes, 'projectileDirection'> {\n\tconst len = Math.sqrt(x * x + y * y);\n\tif (len === 0) return { projectileDirection: { x: 0, y: -1 } };\n\treturn { projectileDirection: { x: x / len, y: y / len } };\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a projectile plugin for ECSpresso.\n *\n * Provides homing and linear projectile movement systems, plus\n * automatic collision-to-damage integration.\n *\n * @example\n * ```typescript\n * // Spawn a homing projectile:\n * ecs.spawn({\n * ...createProjectile(10, 400, turretId),\n * ...createProjectileTarget(enemyId),\n * ...createLocalTransform(x, y),\n * ...createCircleCollider(4),\n * ...collisionLayers.turretProjectile(),\n * sprite: bulletSprite,\n * renderLayer: 'projectiles',\n * });\n * ```\n */\nexport function createProjectilePlugin<G extends string = 'combat'>(\n\toptions?: ProjectilePluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'combat',\n\t\tpriority = 300,\n\t\tphase = 'update',\n\t\tpublishDamage = true,\n\t} = options ?? {};\n\n\treturn definePlugin('projectile')\n\t\t.withComponentTypes<ProjectileComponentTypes>()\n\t\t.withEventTypes<ProjectileEventTypes>()\n\t\t.withLabels<'projectile-homing' | 'projectile-linear' | 'projectile-collision'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tWorldConfigFrom<{}, CollisionEventTypes<string>>\n\t\t>()\n\t\t.install((world) => {\n\t\t\t// Homing projectiles — track target position each frame\n\t\t\tworld\n\t\t\t\t.addSystem('projectile-homing')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('homing', {\n\t\t\t\t\twith: ['projectile', 'projectileTarget', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs, dt }) => {\n\t\t\t\t\tfor (const entity of queries.homing) {\n\t\t\t\t\t\tconst { projectile, projectileTarget, localTransform } = entity.components;\n\n\t\t\t\t\t\t// Target no longer exists — remove projectile\n\t\t\t\t\t\tif (!ecs.getEntity(projectileTarget.entityId)) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst targetTransform = ecs.getComponent(projectileTarget.entityId, 'worldTransform');\n\t\t\t\t\t\tif (!targetTransform) {\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst dx = targetTransform.x - localTransform.x;\n\t\t\t\t\t\tconst dy = targetTransform.y - localTransform.y;\n\t\t\t\t\t\tconst distSq = dx * dx + dy * dy;\n\t\t\t\t\t\tconst step = projectile.speed * dt;\n\n\t\t\t\t\t\tif (distSq <= step * step) {\n\t\t\t\t\t\t\tlocalTransform.x = targetTransform.x;\n\t\t\t\t\t\t\tlocalTransform.y = targetTransform.y;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst dist = Math.sqrt(distSq);\n\t\t\t\t\t\t\tlocalTransform.x += (dx / dist) * step;\n\t\t\t\t\t\t\tlocalTransform.y += (dy / dist) * step;\n\t\t\t\t\t\t\tlocalTransform.rotation = Math.atan2(dy, dx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tecs.markChanged(entity.id, 'localTransform');\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Linear projectiles — move in fixed direction\n\t\t\tworld\n\t\t\t\t.addSystem('projectile-linear')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('linear', {\n\t\t\t\t\twith: ['projectile', 'projectileDirection', 'localTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.linear) {\n\t\t\t\t\t\tconst { projectile, projectileDirection, localTransform } = entity.components;\n\t\t\t\t\t\tconst step = projectile.speed * dt;\n\t\t\t\t\t\tlocalTransform.x += projectileDirection.x * step;\n\t\t\t\t\t\tlocalTransform.y += projectileDirection.y * step;\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t// Collision integration — route collision events to projectileHit + damage\n\t\t\tworld\n\t\t\t\t.addSystem('projectile-collision')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tcollision({ data, ecs }) {\n\t\t\t\t\t\tif (!ecs.getEntity(data.entityA) || !ecs.getEntity(data.entityB)) return;\n\n\t\t\t\t\t\tconst projectileA = ecs.getComponent(data.entityA, 'projectile');\n\t\t\t\t\t\tconst projectileB = ecs.getComponent(data.entityB, 'projectile');\n\n\t\t\t\t\t\tconst isAProjectile = projectileA !== undefined;\n\t\t\t\t\t\tconst projectileData = isAProjectile ? projectileA : projectileB;\n\t\t\t\t\t\tif (!projectileData) return;\n\n\t\t\t\t\t\tconst projectileId = isAProjectile ? data.entityA : data.entityB;\n\t\t\t\t\t\tconst targetId = isAProjectile ? data.entityB : data.entityA;\n\n\t\t\t\t\t\t// Don't hit the entity that fired this projectile\n\t\t\t\t\t\tif (targetId === projectileData.sourceId) return;\n\n\t\t\t\t\t\tecs.eventBus.publish('projectileHit', {\n\t\t\t\t\t\t\tprojectileId,\n\t\t\t\t\t\t\ttargetId,\n\t\t\t\t\t\t\tdamage: projectileData.damage,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (publishDamage) {\n\t\t\t\t\t\t\tecs.eventBus.publish('damage', {\n\t\t\t\t\t\t\t\tentityId: targetId,\n\t\t\t\t\t\t\t\tamount: projectileData.damage,\n\t\t\t\t\t\t\t\tsourceId: projectileData.sourceId,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tecs.commands.removeEntity(projectileId);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n"
6
+ ],
7
+ "mappings": "2PAUA,uBAAS,kBAwFF,SAAS,CAAgB,CAC/B,EACA,EACA,EAC+C,CAC/C,MAAO,CAAE,WAAY,CAAE,SAAQ,QAAO,UAAS,CAAE,EAS3C,SAAS,CAAsB,CAAC,EAAsE,CAC5G,MAAO,CAAE,iBAAkB,CAAE,UAAS,CAAE,EAUlC,SAAS,CAAyB,CAAC,EAAW,EAAkE,CACtH,IAAM,EAAM,KAAK,KAAK,EAAI,EAAI,EAAI,CAAC,EACnC,GAAI,IAAQ,EAAG,MAAO,CAAE,oBAAqB,CAAE,EAAG,EAAG,EAAG,EAAG,CAAE,EAC7D,MAAO,CAAE,oBAAqB,CAAE,EAAG,EAAI,EAAK,EAAG,EAAI,CAAI,CAAE,EAyBnD,SAAS,CAAmD,CAClE,EACC,CACD,IACC,cAAc,SACd,WAAW,IACX,QAAQ,SACR,gBAAgB,IACb,GAAW,CAAC,EAEhB,OAAO,EAAa,YAAY,EAC9B,mBAA6C,EAC7C,eAAqC,EACrC,WAA+E,EAC/E,WAAc,EACd,SAGC,EACD,QAAQ,CAAC,IAAU,CAEnB,EACE,UAAU,mBAAmB,EAC7B,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,aAAc,mBAAoB,gBAAgB,CAC1D,CAAC,EACA,WAAW,EAAG,UAAS,MAAK,QAAS,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,aAAY,mBAAkB,kBAAmB,EAAO,WAGhE,GAAI,CAAC,EAAI,UAAU,EAAiB,QAAQ,EAAG,CAC9C,EAAI,SAAS,aAAa,EAAO,EAAE,EACnC,SAGD,IAAM,EAAkB,EAAI,aAAa,EAAiB,SAAU,gBAAgB,EACpF,GAAI,CAAC,EAAiB,CACrB,EAAI,SAAS,aAAa,EAAO,EAAE,EACnC,SAGD,IAAM,EAAK,EAAgB,EAAI,EAAe,EACxC,EAAK,EAAgB,EAAI,EAAe,EACxC,EAAS,EAAK,EAAK,EAAK,EACxB,EAAO,EAAW,MAAQ,EAEhC,GAAI,GAAU,EAAO,EACpB,EAAe,EAAI,EAAgB,EACnC,EAAe,EAAI,EAAgB,EAC7B,KACN,IAAM,EAAO,KAAK,KAAK,CAAM,EAC7B,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAe,GAAM,EAAK,EAAQ,EAClC,EAAe,SAAW,KAAK,MAAM,EAAI,CAAE,EAE5C,EAAI,YAAY,EAAO,GAAI,gBAAgB,GAE5C,EAGF,EACE,UAAU,mBAAmB,EAC7B,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,aAAc,sBAAuB,gBAAgB,CAC7D,CAAC,EACA,WAAW,EAAG,UAAS,QAAS,CAChC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,aAAY,sBAAqB,kBAAmB,EAAO,WAC7D,EAAO,EAAW,MAAQ,EAChC,EAAe,GAAK,EAAoB,EAAI,EAC5C,EAAe,GAAK,EAAoB,EAAI,GAE7C,EAGF,EACE,UAAU,sBAAsB,EAChC,QAAQ,CAAW,EACnB,iBAAiB,CACjB,SAAS,EAAG,OAAM,OAAO,CACxB,GAAI,CAAC,EAAI,UAAU,EAAK,OAAO,GAAK,CAAC,EAAI,UAAU,EAAK,OAAO,EAAG,OAElE,IAAM,EAAc,EAAI,aAAa,EAAK,QAAS,YAAY,EACzD,EAAc,EAAI,aAAa,EAAK,QAAS,YAAY,EAEzD,EAAgB,IAAgB,OAChC,EAAiB,EAAgB,EAAc,EACrD,GAAI,CAAC,EAAgB,OAErB,IAAM,EAAe,EAAgB,EAAK,QAAU,EAAK,QACnD,EAAW,EAAgB,EAAK,QAAU,EAAK,QAGrD,GAAI,IAAa,EAAe,SAAU,OAQ1C,GANA,EAAI,SAAS,QAAQ,gBAAiB,CACrC,eACA,WACA,OAAQ,EAAe,MACxB,CAAC,EAEG,EACH,EAAI,SAAS,QAAQ,SAAU,CAC9B,SAAU,EACV,OAAQ,EAAe,OACvB,SAAU,EAAe,QAC1B,CAAC,EAGF,EAAI,SAAS,aAAa,CAAY,EAExC,CAAC,EACF",
8
+ "debugId": "D0756C0A27AA8CAC64756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,5 +1,5 @@
1
1
  var L=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(F,k)=>(typeof require<"u"?require:F)[k]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as $}from"ecspresso";function A(j){let F=new Float64Array(j),k=0,q=0;return{push(Q){if(F[k]=Q,k=(k+1)%j,q<j)q++},computeFps(){if(q<2)return 0;let Q=F[(k-1+j)%j]??0,U=F[(k-q+j)%j]??0,z=Q-U;if(z<=0)return 0;return(q-1)/z*1000},computeAverageFrameTime(){if(q<2)return 0;let Q=F[(k-1+j)%j]??0,U=F[(k-q+j)%j]??0,z=Q-U;if(z<=0)return 0;return z/(q-1)},get size(){return q}}}function Y(j){let{systemGroup:F="diagnostics",enableTimingOnInit:k=!0,fpsSampleCount:q=60}=j??{},Q={fps:0,entityCount:0,systemTimings:new Map,phaseTimings:{preUpdate:0,fixedUpdate:0,update:0,postUpdate:0,render:0},averageFrameTime:0},U=A(q);return $("diagnostics").withResourceTypes().withLabels().withGroups().install((z)=>{z.addResource("diagnostics",Q),z.addSystem("diagnostics-collect").setPriority(-999999).inPhase("render").inGroup(F).setOnInitialize((J)=>{if(k)J.enableDiagnostics(!0)}).setOnDetach((J)=>{J.enableDiagnostics(!1)}).setProcess(({ecs:J})=>{let V=performance.now();U.push(V);let K=J.getResource("diagnostics"),H={fps:U.computeFps(),entityCount:J.entityCount,systemTimings:J.systemTimings,phaseTimings:J.phaseTimings,averageFrameTime:U.computeAverageFrameTime()};K.fps=H.fps,K.entityCount=H.entityCount,K.systemTimings=H.systemTimings,K.phaseTimings=H.phaseTimings,K.averageFrameTime=H.averageFrameTime})})}var G={"top-left":"top:8px;left:8px","top-right":"top:8px;right:8px","bottom-left":"bottom:8px;left:8px","bottom-right":"bottom:8px;right:8px"};function _(j,F){let{position:k="top-left",updateInterval:q=200,showSystemTimings:Q=!0,maxSystemsShown:U=10}=F??{},z=document.createElement("div");z.style.cssText=`position:fixed;${G[k]};z-index:999999;background:rgba(0,0,0,0.8);color:#0f0;font:12px/1.4 monospace;padding:8px 12px;border-radius:4px;pointer-events:none;white-space:pre`,document.body.appendChild(z);let J=setInterval(()=>{let V=j.getResource("diagnostics"),K=[`FPS: ${V.fps.toFixed(0)}`,`Frame: ${V.averageFrameTime.toFixed(2)}ms`,`Entities: ${V.entityCount}`],H=V.phaseTimings;if(K.push(`Phases: pre=${H.preUpdate.toFixed(2)} fix=${H.fixedUpdate.toFixed(2)} upd=${H.update.toFixed(2)} post=${H.postUpdate.toFixed(2)} ren=${H.render.toFixed(2)}`),Q&&V.systemTimings.size>0){K.push("--- Systems ---");let Z=[...V.systemTimings.entries()].sort((W,X)=>X[1]-W[1]).slice(0,U);for(let[W,X]of Z)K.push(` ${W}: ${X.toFixed(3)}ms`)}z.textContent=K.join(`
2
2
  `)},q);return()=>{clearInterval(J),z.remove()}}export{Y as createDiagnosticsPlugin,_ as createDiagnosticsOverlay};
3
3
 
4
- //# debugId=93AA75AE725BACCC64756E2164756E21
4
+ //# debugId=D7FE41862EA92B5B64756E2164756E21
5
5
  //# sourceMappingURL=diagnostics.js.map
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/plugins/diagnostics.ts"],
3
+ "sources": ["../src/plugins/debug/diagnostics.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * Diagnostics Plugin for ECSpresso\n *\n * Runtime diagnostics: FPS, entity count, per-system timing, per-phase timing,\n * and an optional DOM overlay for visual debugging.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\n\n// ==================== Types ====================\n\nexport interface DiagnosticsData {\n\tfps: number;\n\tentityCount: number;\n\tsystemTimings: ReadonlyMap<string, number>;\n\tphaseTimings: Readonly<Record<SystemPhase, number>>;\n\taverageFrameTime: number;\n}\n\nexport interface DiagnosticsResourceTypes {\n\tdiagnostics: DiagnosticsData;\n}\n\nexport interface DiagnosticsPluginOptions<G extends string = 'diagnostics'> {\n\t/** System group name (default: 'diagnostics') */\n\tsystemGroup?: G;\n\t/** Enable timing collection on initialize (default: true) */\n\tenableTimingOnInit?: boolean;\n\t/** Number of frames to sample for FPS average (default: 60) */\n\tfpsSampleCount?: number;\n}\n\nexport interface DiagnosticsOverlayOptions {\n\t/** Corner position (default: 'top-left') */\n\tposition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n\t/** Milliseconds between DOM updates (default: 200) */\n\tupdateInterval?: number;\n\t/** Show per-system timings (default: true) */\n\tshowSystemTimings?: boolean;\n\t/** Maximum systems to show in overlay (default: 10) */\n\tmaxSystemsShown?: number;\n}\n\n// ==================== Ring Buffer ====================\n\n/**\n * Fixed-size circular buffer for frame timestamps.\n * Avoids Array.shift() allocation on every frame.\n */\nfunction createRingBuffer(capacity: number) {\n\tconst buffer = new Float64Array(capacity);\n\tlet writeIndex = 0;\n\tlet count = 0;\n\n\treturn {\n\t\tpush(value: number): void {\n\t\t\tbuffer[writeIndex] = value;\n\t\t\twriteIndex = (writeIndex + 1) % capacity;\n\t\t\tif (count < capacity) count++;\n\t\t},\n\n\t\t/** Compute FPS from stored timestamps */\n\t\tcomputeFps(): number {\n\t\t\tif (count < 2) return 0;\n\t\t\tconst newest = buffer[(writeIndex - 1 + capacity) % capacity] ?? 0;\n\t\t\tconst oldest = buffer[(writeIndex - count + capacity) % capacity] ?? 0;\n\t\t\tconst elapsed = newest - oldest;\n\t\t\tif (elapsed <= 0) return 0;\n\t\t\treturn ((count - 1) / elapsed) * 1000;\n\t\t},\n\n\t\t/** Compute average frame time in ms */\n\t\tcomputeAverageFrameTime(): number {\n\t\t\tif (count < 2) return 0;\n\t\t\tconst newest = buffer[(writeIndex - 1 + capacity) % capacity] ?? 0;\n\t\t\tconst oldest = buffer[(writeIndex - count + capacity) % capacity] ?? 0;\n\t\t\tconst elapsed = newest - oldest;\n\t\t\tif (elapsed <= 0) return 0;\n\t\t\treturn elapsed / (count - 1);\n\t\t},\n\n\t\tget size(): number {\n\t\t\treturn count;\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\nexport function createDiagnosticsPlugin<G extends string = 'diagnostics'>(\n\toptions?: DiagnosticsPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'diagnostics',\n\t\tenableTimingOnInit = true,\n\t\tfpsSampleCount = 60,\n\t} = options ?? {};\n\n\tconst initialData: DiagnosticsData = {\n\t\tfps: 0,\n\t\tentityCount: 0,\n\t\tsystemTimings: new Map(),\n\t\tphaseTimings: { preUpdate: 0, fixedUpdate: 0, update: 0, postUpdate: 0, render: 0 },\n\t\taverageFrameTime: 0,\n\t};\n\n\tconst ringBuffer = createRingBuffer(fpsSampleCount);\n\n\treturn definePlugin('diagnostics')\n\t\t.withResourceTypes<DiagnosticsResourceTypes>()\n\t\t.withLabels<'diagnostics-collect'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('diagnostics', initialData);\n\n\t\t\tworld\n\t\t\t\t.addSystem('diagnostics-collect')\n\t\t\t\t.setPriority(-999999)\n\t\t\t\t.inPhase('render')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize((ecs) => {\n\t\t\t\t\tif (enableTimingOnInit) {\n\t\t\t\t\t\tecs.enableDiagnostics(true);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.setOnDetach((ecs) => {\n\t\t\t\t\tecs.enableDiagnostics(false);\n\t\t\t\t})\n\t\t\t\t.setProcess(({ ecs }) => {\n\t\t\t\t\tconst now = performance.now();\n\t\t\t\t\tringBuffer.push(now);\n\n\t\t\t\t\tconst resource = ecs.getResource('diagnostics');\n\t\t\t\t\tconst updated: DiagnosticsData = {\n\t\t\t\t\t\tfps: ringBuffer.computeFps(),\n\t\t\t\t\t\tentityCount: ecs.entityCount,\n\t\t\t\t\t\tsystemTimings: ecs.systemTimings,\n\t\t\t\t\t\tphaseTimings: ecs.phaseTimings,\n\t\t\t\t\t\taverageFrameTime: ringBuffer.computeAverageFrameTime(),\n\t\t\t\t\t};\n\n\t\t\t\t\t// Mutate fields on the existing resource object to avoid allocation\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).fps = updated.fps;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).entityCount = updated.entityCount;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).systemTimings = updated.systemTimings;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).phaseTimings = updated.phaseTimings;\n\t\t\t\t\t(resource as { -readonly [K in keyof DiagnosticsData]: DiagnosticsData[K] }).averageFrameTime = updated.averageFrameTime;\n\t\t\t\t});\n\t\t});\n}\n\n// ==================== Overlay Helper ====================\n\nconst POSITION_STYLES: Record<NonNullable<DiagnosticsOverlayOptions['position']>, string> = {\n\t'top-left': 'top:8px;left:8px',\n\t'top-right': 'top:8px;right:8px',\n\t'bottom-left': 'bottom:8px;left:8px',\n\t'bottom-right': 'bottom:8px;right:8px',\n} as const;\n\n/**\n * Create a DOM overlay that displays diagnostics data.\n * Returns a cleanup function that removes the element and clears the interval.\n *\n * @param ecs An ECSpresso instance with the diagnostics resource\n * @param options Overlay configuration\n * @returns Cleanup function\n */\nexport function createDiagnosticsOverlay<\n\tR extends DiagnosticsResourceTypes,\n>(\n\tecs: { getResource<K extends keyof R>(key: K): R[K] },\n\toptions?: DiagnosticsOverlayOptions,\n): () => void {\n\tconst {\n\t\tposition = 'top-left',\n\t\tupdateInterval = 200,\n\t\tshowSystemTimings = true,\n\t\tmaxSystemsShown = 10,\n\t} = options ?? {};\n\n\tconst el = document.createElement('div');\n\tel.style.cssText = `position:fixed;${POSITION_STYLES[position]};z-index:999999;background:rgba(0,0,0,0.8);color:#0f0;font:12px/1.4 monospace;padding:8px 12px;border-radius:4px;pointer-events:none;white-space:pre`;\n\tdocument.body.appendChild(el);\n\n\tconst intervalId = setInterval(() => {\n\t\tconst d = ecs.getResource('diagnostics' as keyof R) as DiagnosticsData;\n\n\t\tconst lines: string[] = [\n\t\t\t`FPS: ${d.fps.toFixed(0)}`,\n\t\t\t`Frame: ${d.averageFrameTime.toFixed(2)}ms`,\n\t\t\t`Entities: ${d.entityCount}`,\n\t\t];\n\n\t\tconst phases = d.phaseTimings;\n\t\tlines.push(\n\t\t\t`Phases: pre=${phases.preUpdate.toFixed(2)} fix=${phases.fixedUpdate.toFixed(2)} upd=${phases.update.toFixed(2)} post=${phases.postUpdate.toFixed(2)} ren=${phases.render.toFixed(2)}`,\n\t\t);\n\n\t\tif (showSystemTimings && d.systemTimings.size > 0) {\n\t\t\tlines.push('--- Systems ---');\n\t\t\tconst sorted = [...d.systemTimings.entries()]\n\t\t\t\t.sort((a, b) => b[1] - a[1])\n\t\t\t\t.slice(0, maxSystemsShown);\n\t\t\tfor (const [label, ms] of sorted) {\n\t\t\t\tlines.push(` ${label}: ${ms.toFixed(3)}ms`);\n\t\t\t}\n\t\t}\n\n\t\tel.textContent = lines.join('\\n');\n\t}, updateInterval);\n\n\treturn () => {\n\t\tclearInterval(intervalId);\n\t\tel.remove();\n\t};\n}\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "93AA75AE725BACCC64756E2164756E21",
8
+ "debugId": "D7FE41862EA92B5B64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -1,4 +1,4 @@
1
1
  var k=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(O,Q)=>(typeof require<"u"?require:O)[Q]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as A}from"ecspresso";function u(z){return z}function D(){return{keysDown:new Set,keysPressed:[],keysReleased:[],buttonsDown:new Set,buttonsPressed:[],buttonsReleased:[],pointerX:0,pointerY:0,pointerDeltaX:0,pointerDeltaY:0,lastPointerX:0,lastPointerY:0,pointerMoved:!1}}var B=new Set,F=new Set;function R(){return{keysDown:B,keysPressed:B,keysReleased:B,buttonsDown:F,buttonsPressed:F,buttonsReleased:F,pointerX:0,pointerY:0,pointerDeltaX:0,pointerDeltaY:0,actionsActive:B,prevActionsActive:B}}function v(z,O,Q){let V=new Set;for(let[$,W]of Object.entries(z)){let Z=W.keys?.some((H)=>O.has(H))??!1,J=W.buttons?.some((H)=>Q.has(H))??!1;if(Z||J)V.add($)}return V}function P(z,O,Q){let V=new Set(z.keysDown),$=new Set(z.keysPressed),W=new Set(z.keysReleased),Z=new Set(z.buttonsDown),J=new Set(z.buttonsPressed),H=new Set(z.buttonsReleased),j=z.pointerMoved?z.pointerX-z.lastPointerX:0,X=z.pointerMoved?z.pointerY-z.lastPointerY:0,x=v(Q,V,Z),K={keysDown:V,keysPressed:$,keysReleased:W,buttonsDown:Z,buttonsPressed:J,buttonsReleased:H,pointerX:z.pointerX,pointerY:z.pointerY,pointerDeltaX:j,pointerDeltaY:X,actionsActive:x,prevActionsActive:O};return z.keysPressed=[],z.keysReleased=[],z.buttonsPressed=[],z.buttonsReleased=[],z.lastPointerX=z.pointerX,z.lastPointerY=z.pointerY,z.pointerMoved=!1,K}function l(z){let{systemGroup:O="input",priority:Q=100,phase:V="preUpdate",actions:$={},target:W=globalThis,coordinateTransform:Z}=z??{},J=D(),H=R(),j={...$},X=[],x={x:0,y:0},K={x:0,y:0},L={keyboard:{isDown:(q)=>H.keysDown.has(q),justPressed:(q)=>H.keysPressed.has(q),justReleased:(q)=>H.keysReleased.has(q)},pointer:{position:x,delta:K,isDown:(q)=>H.buttonsDown.has(q),justPressed:(q)=>H.buttonsPressed.has(q),justReleased:(q)=>H.buttonsReleased.has(q)},actions:{isActive:(q)=>H.actionsActive.has(q),justActivated:(q)=>H.actionsActive.has(q)&&!H.prevActionsActive.has(q),justDeactivated:(q)=>!H.actionsActive.has(q)&&H.prevActionsActive.has(q)},setActionMap(q){j={...q}},getActionMap(){return{...j}}};function m(q){let C=q;if(C.repeat)return;J.keysDown.add(C.key),J.keysPressed.push(C.key)}function G(q){let C=q;J.keysDown.delete(C.key),J.keysReleased.push(C.key)}function I(q){let C=q;J.buttonsDown.add(C.button),J.buttonsPressed.push(C.button)}function U(q){let C=q;if(Z){let{x:N,y:Y}=Z(C.clientX,C.clientY);J.pointerX=N,J.pointerY=Y}else J.pointerX=C.clientX,J.pointerY=C.clientY;J.pointerMoved=!0}function g(q){let C=q;J.buttonsDown.delete(C.button),J.buttonsReleased.push(C.button)}function f(q,C){W.addEventListener(q,C),X.push(()=>{W.removeEventListener(q,C)})}return A("input").withResourceTypes().withLabels().withGroups().install((q)=>{q.addResource("inputState",L),q.addSystem("input-state").setPriority(Q).inPhase(V).inGroup(O).setOnInitialize(()=>{f("keydown",m),f("keyup",G),f("pointerdown",I),f("pointermove",U),f("pointerup",g)}).setOnDetach(()=>{for(let C of X)C();X.length=0}).setProcess(()=>{let C=H.actionsActive;H=P(J,C,j),x.x=H.pointerX,x.y=H.pointerY,K.x=H.pointerDeltaX,K.y=H.pointerDeltaY})})}export{l as createInputPlugin,u as createActionBinding};
2
2
 
3
- //# debugId=E7F2EB68AF3B74CD64756E2164756E21
3
+ //# debugId=E127744726C61D2164756E2164756E21
4
4
  //# sourceMappingURL=input.js.map
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/plugins/input.ts"],
3
+ "sources": ["../src/plugins/input/input.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * Input Plugin for ECSpresso\n *\n * Provides frame-accurate keyboard, pointer (mouse + touch via PointerEvent),\n * and action mapping input. Resource-only plugin — input is polled via the\n * `inputState` resource. No ECS components or events.\n *\n * DOM events are accumulated between frames and snapshotted once per frame\n * in the system's process step, so all systems see consistent state.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Public Types ====================\n\nexport interface Vec2 {\n\tx: number;\n\ty: number;\n}\n\n// Key codes per the UI Events spec (KeyboardEvent.key values)\n// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n\ntype LowercaseLetter =\n\t| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'\n\t| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';\n\ntype Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';\n\ntype Punctuation =\n\t| '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')'\n\t| '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\\\' | '|'\n\t| ';' | ':' | \"'\" | '\"' | ',' | '<' | '.' | '>' | '/' | '?';\n\ntype ModifierKey =\n\t| 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock'\n\t| 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift'\n\t| 'Super' | 'Symbol' | 'SymbolLock';\n\ntype WhitespaceKey = 'Enter' | 'Tab' | ' ';\n\ntype NavigationKey =\n\t| `Arrow${'Down' | 'Left' | 'Right' | 'Up'}`\n\t| 'End' | 'Home' | 'PageDown' | 'PageUp';\n\ntype EditingKey =\n\t| 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete'\n\t| 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';\n\ntype UIKey =\n\t| 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape'\n\t| 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play'\n\t| 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';\n\ntype DeviceKey =\n\t| 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'Hibernate'\n\t| 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Standby' | 'WakeUp';\n\ntype IMEKey =\n\t| 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert'\n\t| 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious'\n\t| 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate'\n\t| 'Process' | 'SingleCandidate'\n\t| 'HangulMode' | 'HanjaMode' | 'JunjaMode'\n\t| 'Eisu' | 'Hankaku' | 'Hiragana' | 'HiraganaKatakana' | 'KanaMode'\n\t| 'KanjiMode' | 'Katakana' | 'Romaji' | 'Zenkaku' | 'ZenkakuHankaku';\n\ntype FunctionKey =\n\t| `F${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24}`\n\t| 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';\n\ntype PhoneKey =\n\t| 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall'\n\t| 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial'\n\t| 'Notification' | 'MannerMode' | 'VoiceDial';\n\ntype MultimediaKey =\n\t| 'ChannelDown' | 'ChannelUp'\n\t| `Media${\n\t\t'FastForward' | 'Pause' | 'Play' | 'PlayPause'\n\t\t| 'Record' | 'Rewind' | 'Stop' | 'TrackNext' | 'TrackPrevious'\n\t}`;\n\ntype AudioKey =\n\t| `Audio${\n\t\t'BalanceLeft' | 'BalanceRight' | 'BassDown' | 'BassBoostDown'\n\t\t| 'BassBoostToggle' | 'BassBoostUp' | 'BassUp' | 'FaderFront' | 'FaderRear'\n\t\t| 'SurroundModeNext' | 'TrebleDown' | 'TrebleUp'\n\t\t| 'VolumeDown' | 'VolumeMute' | 'VolumeUp'\n\t}`\n\t| `Microphone${'Toggle' | 'VolumeDown' | 'VolumeMute' | 'VolumeUp'}`;\n\ntype TVKey =\n\t| 'TV'\n\t| `TV${\n\t\t'3DMode' | 'AntennaCable' | 'AudioDescription' | 'AudioDescriptionMixDown'\n\t\t| 'AudioDescriptionMixUp' | 'ContentsMenu' | 'DataService' | 'Input'\n\t\t| 'InputComponent1' | 'InputComponent2' | 'InputComposite1' | 'InputComposite2'\n\t\t| 'InputHDMI1' | 'InputHDMI2' | 'InputHDMI3' | 'InputHDMI4' | 'InputVGA1'\n\t\t| 'MediaContext' | 'Network' | 'NumberEntry' | 'Power' | 'RadioService'\n\t\t| 'Satellite' | 'SatelliteBS' | 'SatelliteCS' | 'SatelliteToggle'\n\t\t| 'TerrestrialAnalog' | 'TerrestrialDigital' | 'Timer'\n\t}`;\n\ntype MediaControllerKey =\n\t| 'AVRInput' | 'AVRPower'\n\t| `Color${'F0Red' | 'F1Green' | 'F2Yellow' | 'F3Blue' | 'F4Grey' | 'F5Brown'}`\n\t| 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit'\n\t| `Favorite${'Clear' | 'Recall' | 'Store'}${0 | 1 | 2 | 3}`\n\t| 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay'\n\t| 'Link' | 'ListProgram' | 'LiveContent' | 'Lock'\n\t| `Media${\n\t\t'Apps' | 'AudioTrack' | 'Last' | 'SkipBackward'\n\t\t| 'SkipForward' | 'StepBackward' | 'StepForward' | 'TopMenu'\n\t}`\n\t| `Navigate${'In' | 'Next' | 'Out' | 'Previous'}`\n\t| 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing'\n\t| `PinP${'Down' | 'Move' | 'Toggle' | 'Up'}`\n\t| `PlaySpeed${'Down' | 'Reset' | 'Up'}`\n\t| 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass'\n\t| 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle'\n\t| 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext'\n\t| 'VideoModeNext' | 'Wink' | 'ZoomToggle';\n\ntype SpeechKey = 'SpeechCorrectionList' | 'SpeechInputToggle';\n\ntype DocumentKey =\n\t| 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck'\n\t| 'MailForward' | 'MailReply' | 'MailSend';\n\ntype LaunchKey = `Launch${\n\t| 'Calculator' | 'Calendar' | 'Contacts' | 'Mail' | 'MediaPlayer'\n\t| 'MusicPlayer' | 'MyComputer' | 'Phone' | 'ScreenSaver' | 'Spreadsheet'\n\t| 'WebBrowser' | 'WebCam' | 'WordProcessor'\n\t| `Application${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16}`\n}`;\n\ntype BrowserKey = `Browser${'Back' | 'Favorites' | 'Forward' | 'Home' | 'Refresh' | 'Search' | 'Stop'}`;\n\ntype NumpadKey = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Divide' | 'Subtract' | 'Separator';\n\nexport type KeyCode =\n\t| LowercaseLetter | Uppercase<LowercaseLetter> | Digit | Punctuation\n\t| ModifierKey | WhitespaceKey | NavigationKey | EditingKey | UIKey | DeviceKey\n\t| IMEKey | FunctionKey | PhoneKey | MultimediaKey | AudioKey | TVKey\n\t| MediaControllerKey | SpeechKey | DocumentKey | LaunchKey | BrowserKey | NumpadKey\n\t| 'Unidentified' | 'Dead';\n\nexport interface KeyboardState {\n\tisDown(key: KeyCode): boolean;\n\tjustPressed(key: KeyCode): boolean;\n\tjustReleased(key: KeyCode): boolean;\n}\n\nexport interface PointerState {\n\treadonly position: Readonly<Vec2>;\n\treadonly delta: Readonly<Vec2>;\n\tisDown(button: number): boolean;\n\tjustPressed(button: number): boolean;\n\tjustReleased(button: number): boolean;\n}\n\nexport interface ActionState<A extends string = string> {\n\tisActive(action: A): boolean;\n\tjustActivated(action: A): boolean;\n\tjustDeactivated(action: A): boolean;\n}\n\nexport interface InputState<A extends string = string> {\n\treadonly keyboard: KeyboardState;\n\treadonly pointer: PointerState;\n\treadonly actions: ActionState<A>;\n\tsetActionMap(actions: ActionMap<A>): void;\n\tgetActionMap(): Readonly<ActionMap<A>>;\n}\n\nexport interface ActionBinding {\n\tkeys?: KeyCode[];\n\tbuttons?: number[];\n}\n\nexport type ActionMap<A extends string = string> = Record<A, ActionBinding>;\n\nexport interface InputResourceTypes<A extends string = string> {\n\tinputState: InputState<A>;\n}\n\nexport interface InputPluginOptions<A extends string = string, G extends string = 'input'> extends BasePluginOptions<G> {\n\t/** Initial action mappings */\n\tactions?: ActionMap<A>;\n\t/** EventTarget to attach listeners to (default: globalThis). Pass a custom target for testability. */\n\ttarget?: EventTarget;\n\t/**\n\t * Optional conversion from raw DOM client coordinates to the space `inputState.pointer.position` should report.\n\t * Renderer-agnostic: wire to `clientToLogical(...)` from renderer2D when using `screenScale`, or to a renderer-specific helper.\n\t * When omitted, pointer coords remain raw `clientX`/`clientY` (not canvas-relative).\n\t */\n\tcoordinateTransform?: (clientX: number, clientY: number) => { x: number; y: number };\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a single action binding.\n *\n * @param binding The binding configuration\n * @returns The same binding object\n */\nexport function createActionBinding(binding: ActionBinding): ActionBinding {\n\treturn binding;\n}\n\n// ==================== Internal Types ====================\n\ninterface RawInputState {\n\tkeysDown: Set<string>;\n\tkeysPressed: string[];\n\tkeysReleased: string[];\n\tbuttonsDown: Set<number>;\n\tbuttonsPressed: number[];\n\tbuttonsReleased: number[];\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tlastPointerX: number;\n\tlastPointerY: number;\n\tpointerMoved: boolean;\n}\n\ninterface FrameSnapshot {\n\tkeysDown: ReadonlySet<string>;\n\tkeysPressed: ReadonlySet<string>;\n\tkeysReleased: ReadonlySet<string>;\n\tbuttonsDown: ReadonlySet<number>;\n\tbuttonsPressed: ReadonlySet<number>;\n\tbuttonsReleased: ReadonlySet<number>;\n\tpointerX: number;\n\tpointerY: number;\n\tpointerDeltaX: number;\n\tpointerDeltaY: number;\n\tactionsActive: ReadonlySet<string>;\n\tprevActionsActive: ReadonlySet<string>;\n}\n\n// ==================== Plugin Factory ====================\n\nfunction createRawInputState(): RawInputState {\n\treturn {\n\t\tkeysDown: new Set(),\n\t\tkeysPressed: [],\n\t\tkeysReleased: [],\n\t\tbuttonsDown: new Set(),\n\t\tbuttonsPressed: [],\n\t\tbuttonsReleased: [],\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tlastPointerX: 0,\n\t\tlastPointerY: 0,\n\t\tpointerMoved: false,\n\t};\n}\n\nconst EMPTY_SET_STRING: ReadonlySet<string> = new Set<string>();\nconst EMPTY_SET_NUMBER: ReadonlySet<number> = new Set<number>();\n\nfunction createEmptySnapshot(): FrameSnapshot {\n\treturn {\n\t\tkeysDown: EMPTY_SET_STRING,\n\t\tkeysPressed: EMPTY_SET_STRING,\n\t\tkeysReleased: EMPTY_SET_STRING,\n\t\tbuttonsDown: EMPTY_SET_NUMBER,\n\t\tbuttonsPressed: EMPTY_SET_NUMBER,\n\t\tbuttonsReleased: EMPTY_SET_NUMBER,\n\t\tpointerX: 0,\n\t\tpointerY: 0,\n\t\tpointerDeltaX: 0,\n\t\tpointerDeltaY: 0,\n\t\tactionsActive: EMPTY_SET_STRING,\n\t\tprevActionsActive: EMPTY_SET_STRING,\n\t};\n}\n\nfunction computeActiveActions(\n\tactionMap: ActionMap,\n\tkeysDown: ReadonlySet<string>,\n\tbuttonsDown: ReadonlySet<number>,\n): Set<string> {\n\tconst active = new Set<string>();\n\tfor (const [name, binding] of Object.entries(actionMap)) {\n\t\tconst keyActive = binding.keys?.some((k) => keysDown.has(k)) ?? false;\n\t\tconst buttonActive = binding.buttons?.some((b) => buttonsDown.has(b)) ?? false;\n\t\tif (keyActive || buttonActive) {\n\t\t\tactive.add(name);\n\t\t}\n\t}\n\treturn active;\n}\n\nfunction snapshotRaw(raw: RawInputState, prevActionsActive: ReadonlySet<string>, actionMap: ActionMap): FrameSnapshot {\n\tconst keysDown = new Set(raw.keysDown);\n\tconst keysPressed = new Set(raw.keysPressed);\n\tconst keysReleased = new Set(raw.keysReleased);\n\tconst buttonsDown = new Set(raw.buttonsDown);\n\tconst buttonsPressed = new Set(raw.buttonsPressed);\n\tconst buttonsReleased = new Set(raw.buttonsReleased);\n\n\tconst pointerDeltaX = raw.pointerMoved ? raw.pointerX - raw.lastPointerX : 0;\n\tconst pointerDeltaY = raw.pointerMoved ? raw.pointerY - raw.lastPointerY : 0;\n\n\tconst actionsActive = computeActiveActions(actionMap, keysDown, buttonsDown);\n\n\tconst snapshot: FrameSnapshot = {\n\t\tkeysDown,\n\t\tkeysPressed,\n\t\tkeysReleased,\n\t\tbuttonsDown,\n\t\tbuttonsPressed,\n\t\tbuttonsReleased,\n\t\tpointerX: raw.pointerX,\n\t\tpointerY: raw.pointerY,\n\t\tpointerDeltaX,\n\t\tpointerDeltaY,\n\t\tactionsActive,\n\t\tprevActionsActive,\n\t};\n\n\t// Clear accumulation buffers\n\traw.keysPressed = [];\n\traw.keysReleased = [];\n\traw.buttonsPressed = [];\n\traw.buttonsReleased = [];\n\traw.lastPointerX = raw.pointerX;\n\traw.lastPointerY = raw.pointerY;\n\traw.pointerMoved = false;\n\n\treturn snapshot;\n}\n\n/**\n * Create an input plugin for ECSpresso.\n *\n * This plugin provides:\n * - Frame-accurate keyboard state (isDown, justPressed, justReleased)\n * - Pointer position/delta and button state (mouse + touch via PointerEvent)\n * - Named action mapping with runtime remapping\n * - Automatic listener cleanup on detach\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createInputPlugin({\n * actions: {\n * jump: { keys: [' ', 'ArrowUp'] },\n * shoot: { keys: ['z'], buttons: [0] },\n * },\n * }))\n * .build();\n *\n * // In a system:\n * const input = ecs.getResource('inputState');\n * if (input.actions.justActivated('jump')) { ... }\n * if (input.keyboard.isDown('ArrowRight')) { ... }\n * ```\n */\nexport function createInputPlugin<A extends string = string, G extends string = 'input'>(\n\toptions?: InputPluginOptions<A, G>\n) {\n\tconst {\n\t\tsystemGroup = 'input',\n\t\tpriority = 100,\n\t\tphase = 'preUpdate',\n\t\tactions: initialActions = {},\n\t\ttarget = globalThis,\n\t\tcoordinateTransform,\n\t} = options ?? {};\n\n\t// Closure state\n\tconst raw = createRawInputState();\n\tlet snapshot = createEmptySnapshot();\n\tlet actionMap: ActionMap = { ...initialActions };\n\tconst cleanupFns: Array<() => void> = [];\n\n\t// The position/delta objects exposed via the resource.\n\t// Updated in-place each frame to avoid allocations.\n\tconst position: Vec2 = { x: 0, y: 0 };\n\tconst delta: Vec2 = { x: 0, y: 0 };\n\n\t// Build the InputState resource that closes over snapshot\n\tconst keyboard: KeyboardState = {\n\t\tisDown: (key) => snapshot.keysDown.has(key),\n\t\tjustPressed: (key) => snapshot.keysPressed.has(key),\n\t\tjustReleased: (key) => snapshot.keysReleased.has(key),\n\t};\n\n\tconst pointer: PointerState = {\n\t\tposition,\n\t\tdelta,\n\t\tisDown: (button) => snapshot.buttonsDown.has(button),\n\t\tjustPressed: (button) => snapshot.buttonsPressed.has(button),\n\t\tjustReleased: (button) => snapshot.buttonsReleased.has(button),\n\t};\n\n\tconst actionState: ActionState<A> = {\n\t\tisActive: (action) => snapshot.actionsActive.has(action),\n\t\tjustActivated: (action) =>\n\t\t\tsnapshot.actionsActive.has(action) && !snapshot.prevActionsActive.has(action),\n\t\tjustDeactivated: (action) =>\n\t\t\t!snapshot.actionsActive.has(action) && snapshot.prevActionsActive.has(action),\n\t};\n\n\tconst inputState: InputState<A> = {\n\t\tkeyboard,\n\t\tpointer,\n\t\tactions: actionState,\n\t\tsetActionMap(newMap) {\n\t\t\tactionMap = { ...newMap };\n\t\t},\n\t\tgetActionMap() {\n\t\t\treturn { ...actionMap } as ActionMap<A>;\n\t\t},\n\t};\n\n\t// DOM event handlers\n\tfunction onKeyDown(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\tif (ke.repeat) return;\n\t\traw.keysDown.add(ke.key);\n\t\traw.keysPressed.push(ke.key);\n\t}\n\n\tfunction onKeyUp(e: Event) {\n\t\tconst ke = e as KeyboardEvent;\n\t\traw.keysDown.delete(ke.key);\n\t\traw.keysReleased.push(ke.key);\n\t}\n\n\tfunction onPointerDown(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.add(pe.button);\n\t\traw.buttonsPressed.push(pe.button);\n\t}\n\n\tfunction onPointerMove(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\tif (coordinateTransform) {\n\t\t\tconst { x, y } = coordinateTransform(pe.clientX, pe.clientY);\n\t\t\traw.pointerX = x;\n\t\t\traw.pointerY = y;\n\t\t} else {\n\t\t\traw.pointerX = pe.clientX;\n\t\t\traw.pointerY = pe.clientY;\n\t\t}\n\t\traw.pointerMoved = true;\n\t}\n\n\tfunction onPointerUp(e: Event) {\n\t\tconst pe = e as PointerEvent;\n\t\traw.buttonsDown.delete(pe.button);\n\t\traw.buttonsReleased.push(pe.button);\n\t}\n\n\tfunction addListener(type: string, handler: (e: Event) => void) {\n\t\ttarget.addEventListener(type, handler);\n\t\tcleanupFns.push(() => { target.removeEventListener(type, handler); });\n\t}\n\n\treturn definePlugin('input')\n\t\t.withResourceTypes<InputResourceTypes<A>>()\n\t\t.withLabels<'input-state'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('inputState', inputState);\n\n\t\t\tworld\n\t\t\t\t.addSystem('input-state')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setOnInitialize(() => {\n\t\t\t\t\taddListener('keydown', onKeyDown);\n\t\t\t\t\taddListener('keyup', onKeyUp);\n\t\t\t\t\taddListener('pointerdown', onPointerDown);\n\t\t\t\t\taddListener('pointermove', onPointerMove);\n\t\t\t\t\taddListener('pointerup', onPointerUp);\n\t\t\t\t})\n\t\t\t\t.setOnDetach(() => {\n\t\t\t\t\tfor (const cleanup of cleanupFns) {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t}\n\t\t\t\t\tcleanupFns.length = 0;\n\t\t\t\t})\n\t\t\t\t.setProcess(() => {\n\t\t\t\t\tconst prevActionsActive = snapshot.actionsActive;\n\t\t\t\t\tsnapshot = snapshotRaw(raw, prevActionsActive, actionMap);\n\n\t\t\t\t\t// Update the exposed position/delta objects in-place\n\t\t\t\t\tposition.x = snapshot.pointerX;\n\t\t\t\t\tposition.y = snapshot.pointerY;\n\t\t\t\t\tdelta.x = snapshot.pointerDeltaX;\n\t\t\t\t\tdelta.y = snapshot.pointerDeltaY;\n\t\t\t\t});\n\t\t});\n}\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "E7F2EB68AF3B74CD64756E2164756E21",
8
+ "debugId": "E127744726C61D2164756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Selection Plugin for ECSpresso
3
+ *
4
+ * Provides pointer-driven entity selection via box-drag and click.
5
+ * Entities with a `selectable` component can be selected by the user.
6
+ * Selected entities receive a `selected` component that other systems
7
+ * can query for.
8
+ *
9
+ * Requires the input plugin (for pointer state) and the renderer2D plugin
10
+ * (for graphics rendering of the selection box).
11
+ *
12
+ * Camera-aware: when a `cameraState` resource is present (from the camera
13
+ * plugin), pointer coordinates are automatically converted to world space
14
+ * for hit-testing. The selection box overlay remains in screen space.
15
+ */
16
+ import { type BasePluginOptions } from 'ecspresso';
17
+ import type { WorldConfigFrom } from 'ecspresso';
18
+ import type { InputResourceTypes } from './input';
19
+ import type { Renderer2DComponentTypes, Renderer2DResourceTypes } from '../rendering/renderer2D';
20
+ /**
21
+ * Component types provided by the selection plugin.
22
+ */
23
+ export interface SelectionComponentTypes {
24
+ /** Tag marking an entity as eligible for selection */
25
+ selectable: true;
26
+ /** Tag marking an entity as currently selected (added/removed dynamically) */
27
+ selected: true;
28
+ }
29
+ /**
30
+ * Internal state tracking the current drag selection.
31
+ */
32
+ export interface SelectionState {
33
+ dragStart: {
34
+ x: number;
35
+ y: number;
36
+ };
37
+ boxEntityId: number | null;
38
+ }
39
+ /**
40
+ * Resource types provided by the selection plugin.
41
+ */
42
+ export interface SelectionResourceTypes {
43
+ selectionState: SelectionState;
44
+ }
45
+ /**
46
+ * WorldConfig representing the selection plugin's provided types.
47
+ */
48
+ export type SelectionWorldConfig = WorldConfigFrom<SelectionComponentTypes, {}, SelectionResourceTypes>;
49
+ type SelectionRequires = WorldConfigFrom<Renderer2DComponentTypes, {}, InputResourceTypes & Renderer2DResourceTypes>;
50
+ /**
51
+ * Configuration options for the selection plugin.
52
+ */
53
+ export interface SelectionPluginOptions<G extends string = 'selection'> extends BasePluginOptions<G> {
54
+ /** Minimum drag distance (px) to trigger box select vs click select (default: 5) */
55
+ clickThreshold?: number;
56
+ /** Selection box fill color (default: 0x00FF00) */
57
+ boxFillColor?: number;
58
+ /** Selection box fill alpha (default: 0.15) */
59
+ boxFillAlpha?: number;
60
+ /** Selection box stroke color (default: 0x00FF00) */
61
+ boxStrokeColor?: number;
62
+ /** Selection box stroke alpha (default: 0.8) */
63
+ boxStrokeAlpha?: number;
64
+ /** Tint applied to selected entities' sprites (default: 0x44FF44) */
65
+ selectedTint?: number;
66
+ /** Render layer for the selection box entity (default: undefined) */
67
+ renderLayer?: string;
68
+ }
69
+ /**
70
+ * Create a selectable component.
71
+ *
72
+ * @returns Component object suitable for spreading into spawn()
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * ecs.spawn({
77
+ * ...createTransform(100, 200),
78
+ * sprite,
79
+ * ...createSelectable(),
80
+ * });
81
+ * ```
82
+ */
83
+ export declare function createSelectable(): Pick<SelectionComponentTypes, 'selectable'>;
84
+ /**
85
+ * Create a selection plugin for ECSpresso.
86
+ *
87
+ * Provides:
88
+ * - Box-drag selection (left-click drag to select multiple entities)
89
+ * - Click selection (left-click to select a single entity)
90
+ * - Visual feedback (configurable sprite tint for selected entities)
91
+ * - Selection box overlay (rendered as a PixiJS Graphics entity)
92
+ * - Automatic camera-awareness when cameraState resource is present
93
+ *
94
+ * Requires the input plugin and renderer2D plugin to be installed.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const ecs = ECSpresso.create()
99
+ * .withPlugin(createRenderer2DPlugin({ renderLayers: ['game', 'ui'] }))
100
+ * .withPlugin(createInputPlugin())
101
+ * .withPlugin(createSelectionPlugin({ renderLayer: 'ui' }))
102
+ * .build();
103
+ *
104
+ * await ecs.initialize();
105
+ *
106
+ * ecs.spawn({
107
+ * sprite,
108
+ * ...createTransform(100, 200),
109
+ * ...createSelectable(),
110
+ * });
111
+ * ```
112
+ */
113
+ export declare function createSelectionPlugin<G extends string = 'selection'>(options?: SelectionPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, SelectionComponentTypes>, SelectionResourceTypes>, SelectionRequires, "selection-input" | "selection-visual", G, never, never>;
114
+ export {};
@@ -0,0 +1,4 @@
1
+ var o=((V)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(V,{get:(G,j)=>(typeof require<"u"?require:G)[j]}):V)(function(V){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+V+'" is not supported')});import{definePlugin as m}from"ecspresso";var u={traumaDecay:1,maxOffsetX:10,maxOffsetY:10,maxRotation:0.05},x={smoothing:5,deadzoneX:0,deadzoneY:0,offsetX:0,offsetY:0};function t(V,G,j){let H=V-(j.x+j.shakeOffsetX),C=G-(j.y+j.shakeOffsetY),D=-(j.rotation+j.shakeRotation),q=Math.cos(D),L=Math.sin(D),M=H*q-C*L,W=H*L+C*q;return{x:M*j.zoom+j.viewportWidth/2,y:W*j.zoom+j.viewportHeight/2}}function S(V,G,j){let H=(V-j.viewportWidth/2)/j.zoom,C=(G-j.viewportHeight/2)/j.zoom,D=j.rotation+j.shakeRotation,q=Math.cos(D),L=Math.sin(D),M=H*q-C*L,W=H*L+C*q;return{x:M+j.x+j.shakeOffsetX,y:W+j.y+j.shakeOffsetY}}function i(V){return typeof V==="number"?V:V.id}function p(V){let G=V===!0?{}:V;return{trauma:0,traumaDecay:G.traumaDecay??u.traumaDecay,maxOffsetX:G.maxOffsetX??u.maxOffsetX,maxOffsetY:G.maxOffsetY??u.maxOffsetY,maxRotation:G.maxRotation??u.maxRotation}}function n(V){if(Array.isArray(V))return{minX:V[0],minY:V[1],maxX:V[2],maxY:V[3]};return{...V}}function d(V){return{smoothing:V?.smoothing??x.smoothing,deadzoneX:V?.deadzoneX??x.deadzoneX,deadzoneY:V?.deadzoneY??x.deadzoneY,offsetX:V?.offsetX??x.offsetX,offsetY:V?.offsetY??x.offsetY}}function e(V){let{viewportWidth:G=800,viewportHeight:j=600,initial:H,follow:C,shake:D,bounds:q,zoom:L,systemGroup:M="camera",phase:W="postUpdate",randomFn:h=Math.random}=V??{};return m("camera").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install((b)=>{let R={x:H?.x??0,y:H?.y??0,zoom:H?.zoom??1,rotation:H?.rotation??0,shakeOffsetX:0,shakeOffsetY:0,shakeRotation:0,viewportWidth:G,viewportHeight:j,entityId:-1,follow:()=>{},unfollow:()=>{},setPosition:()=>{},setZoom:()=>{},setRotation:()=>{},setBounds:()=>{},clearBounds:()=>{},addTrauma:()=>{}};if(b.addResource("cameraState",R),b.addSystem("camera-init").inGroup(M).setOnInitialize(($)=>{let P=$.spawn({camera:{x:H?.x??0,y:H?.y??0,zoom:H?.zoom??1,rotation:H?.rotation??0}});if(C)$.addComponent(P.id,"cameraFollow",{target:-1,...d(C)});if(D)$.addComponent(P.id,"cameraShake",p(D));if(q)$.addComponent(P.id,"cameraBounds",n(q));R.entityId=P.id,R.follow=(Q,J)=>{let I={target:i(Q),...d(J)},N=$.getComponent(R.entityId,"cameraFollow");if(N)N.target=I.target,N.smoothing=I.smoothing,N.deadzoneX=I.deadzoneX,N.deadzoneY=I.deadzoneY,N.offsetX=I.offsetX,N.offsetY=I.offsetY;else $.addComponent(R.entityId,"cameraFollow",I)},R.unfollow=()=>{if($.getComponent(R.entityId,"cameraFollow"))$.removeComponent(R.entityId,"cameraFollow")},R.setPosition=(Q,J)=>{let K=$.getComponent(R.entityId,"camera");if(!K)return;K.x=Q,K.y=J},R.setZoom=(Q)=>{let J=$.getComponent(R.entityId,"camera");if(!J)return;J.zoom=Q},R.setRotation=(Q)=>{let J=$.getComponent(R.entityId,"camera");if(!J)return;J.rotation=Q},R.setBounds=(Q,J,K,I)=>{let N=$.getComponent(R.entityId,"cameraBounds");if(N)N.minX=Q,N.minY=J,N.maxX=K,N.maxY=I;else $.addComponent(R.entityId,"cameraBounds",{minX:Q,minY:J,maxX:K,maxY:I})},R.clearBounds=()=>{if($.getComponent(R.entityId,"cameraBounds"))$.removeComponent(R.entityId,"cameraBounds")},R.addTrauma=(Q)=>{let J=$.getComponent(R.entityId,"cameraShake");if(J)J.trauma=Math.min(1,Math.max(0,J.trauma+Q));else $.addComponent(R.entityId,"cameraShake",{...p(!0),trauma:Math.min(1,Math.max(0,Q))})}}),b.addSystem("camera-follow").setPriority(400).inPhase(W).inGroup(M).addQuery("cameras",{with:["camera","cameraFollow"]}).setProcess(({queries:$,dt:P,ecs:Q})=>{let J=Math.min(1,P);for(let K of $.cameras){let{camera:I,cameraFollow:N}=K.components;if(N.target<0)continue;let Z;try{Z=Q.getComponent(N.target,"worldTransform")}catch{continue}if(!Z)continue;let U=Z.x+N.offsetX,O=Z.y+N.offsetY,_=U-I.x,B=O-I.y;if(Math.abs(_)>N.deadzoneX){let E=_>0?1:-1,v=_-E*N.deadzoneX,f=Math.min(1,N.smoothing*J);I.x+=v*f}if(Math.abs(B)>N.deadzoneY){let E=B>0?1:-1,v=B-E*N.deadzoneY,f=Math.min(1,N.smoothing*J);I.y+=v*f}}}),b.addSystem("camera-shake-update").setPriority(390).inPhase(W).inGroup(M).addQuery("shakeCameras",{with:["camera","cameraShake"]}).setProcess(({queries:$,dt:P})=>{for(let Q of $.shakeCameras){let{cameraShake:J}=Q.components;J.trauma=Math.max(0,J.trauma-J.traumaDecay*P)}}),b.addSystem("camera-bounds").setPriority(380).inPhase(W).inGroup(M).addQuery("boundedCameras",{with:["camera","cameraBounds"]}).setProcess(({queries:$})=>{for(let P of $.boundedCameras){let{camera:Q,cameraBounds:J}=P.components,K=R.viewportWidth/(2*Q.zoom),I=R.viewportHeight/(2*Q.zoom),N=J.minX+K,Z=J.maxX-K,U=J.minY+I,O=J.maxY-I;if(N>Z)Q.x=(J.minX+J.maxX)/2;else Q.x=Math.max(N,Math.min(Z,Q.x));if(U>O)Q.y=(J.minY+J.maxY)/2;else Q.y=Math.max(U,Math.min(O,Q.y))}}),b.addSystem("camera-state-sync").setPriority(370).inPhase(W).inGroup(M).setProcess(({ecs:$})=>{let P=$.getComponent(R.entityId,"camera");if(!P){R.x=0,R.y=0,R.zoom=1,R.rotation=0,R.shakeOffsetX=0,R.shakeOffsetY=0,R.shakeRotation=0;return}R.x=P.x,R.y=P.y,R.zoom=P.zoom,R.rotation=P.rotation;let Q=$.getComponent(R.entityId,"cameraShake");if(Q&&Q.trauma>0){let J=Q.trauma*Q.trauma;R.shakeOffsetX=Q.maxOffsetX*J*(h()*2-1),R.shakeOffsetY=Q.maxOffsetY*J*(h()*2-1),R.shakeRotation=Q.maxRotation*J*(h()*2-1)}else R.shakeOffsetX=0,R.shakeOffsetY=0,R.shakeRotation=0}),L){let I=function(N){N.preventDefault(),J+=Math.sign(N.deltaY)},{zoomStep:$=0.1,minZoom:P=0.1,maxZoom:Q=10}=L,J=0,K=!1;b.addSystem("camera-zoom").setPriority(410).inPhase("preUpdate").inGroup(M).addQuery("cameras",{with:["camera"]}).setOnInitialize((N)=>{let Z=N.tryGetResource("inputState"),U=N.tryGetResource("pixiApp");if(!Z||!U){console.error("[camera] zoom requires the input plugin and renderer2D plugin. Zoom will be disabled.");return}U.canvas.addEventListener("wheel",I,{passive:!1}),K=!0}).setOnDetach((N)=>{if(!K)return;let Z=N.tryGetResource("pixiApp");if(Z)Z.canvas.removeEventListener("wheel",I)}).setProcess(({queries:N,ecs:Z})=>{if(!K||J===0)return;let U=J;J=0;let[O]=N.cameras;if(!O)return;let _=O.components.camera,B=Z.tryGetResource("inputState");if(!B)return;let E=S(B.pointer.position.x,B.pointer.position.y,R),v=U>0?1-$:1+$;_.zoom=Math.max(P,Math.min(Q,_.zoom*Math.pow(v,Math.abs(U)))),_.x=E.x-(B.pointer.position.x-R.viewportWidth/2)/_.zoom,_.y=E.y-(B.pointer.position.y-R.viewportHeight/2)/_.zoom})}})}import{Graphics as c}from"pixi.js";import{definePlugin as r}from"ecspresso";function $J(){return{selectable:!0}}function VJ(V){let{systemGroup:G="selection",priority:j=100,phase:H="preUpdate",clickThreshold:C=5,boxFillColor:D=65280,boxFillAlpha:q=0.15,boxStrokeColor:L=65280,boxStrokeAlpha:M=0.8,selectedTint:W=4521796,renderLayer:h}=V??{},b={color:D,alpha:q},R={color:L,width:1.5,alpha:M};return r("selection").withComponentTypes().withResourceTypes().withLabels().withGroups().requires().install(($)=>{$.addResource("selectionState",{dragStart:{x:0,y:0},boxEntityId:null});let P=null;$.addSystem("selection-input").setPriority(j).inPhase(H).inGroup(G).addQuery("selectables",{with:["selectable","worldTransform"]}).addQuery("currentlySelected",{with:["selected"]}).withResources(["inputState","selectionState","pixiApp"]).setOnInitialize((Q)=>{let J=Q.getResource("pixiApp");P=(K)=>K.preventDefault(),J.canvas.addEventListener("contextmenu",P)}).setOnDetach((Q)=>{if(!P)return;Q.getResource("pixiApp").canvas.removeEventListener("contextmenu",P),P=null}).setProcess(({queries:Q,ecs:J,resources:K})=>{let{inputState:I,selectionState:N}=K,Z=I.pointer;if(Z.justPressed(0)){if(N.boxEntityId!==null)J.commands.removeEntity(N.boxEntityId);N.dragStart.x=Z.position.x,N.dragStart.y=Z.position.y;let z=J.spawn({graphics:new c});if(h)J.addComponent(z.id,"renderLayer",h);N.boxEntityId=z.id}if(Z.isDown(0)&&N.boxEntityId!==null){let z=J.getComponent(N.boxEntityId,"graphics");if(!z)return;let A=N.dragStart.x,k=N.dragStart.y,F=Z.position.x,X=Z.position.y,Y=Math.min(A,F),T=Math.min(k,X),g=Math.abs(F-A),l=Math.abs(X-k);z.clear(),z.rect(Y,T,g,l),z.fill(b),z.stroke(R)}if(!Z.justReleased(0)||N.boxEntityId===null)return;let U=N.dragStart.x,O=N.dragStart.y,_=Z.position.x,B=Z.position.y,E=Math.abs(_-U),v=Math.abs(B-O);for(let z of Q.currentlySelected)J.removeComponent(z.id,"selected");let f=E<C&&v<C,w=J.tryGetResource("cameraState"),y=w?S(_,B,w):{x:_,y:B};if(f){let A=null,k=1/0;for(let F of Q.selectables){let{worldTransform:X}=F.components,Y=X.x-y.x,T=X.y-y.y,g=Y*Y+T*T;if(g<400&&g<k)k=g,A=F.id}if(A!==null)J.addComponent(A,"selected",!0)}else{let z=w?S(U,O,w):{x:U,y:O},A=Math.min(z.x,y.x),k=Math.max(z.x,y.x),F=Math.min(z.y,y.y),X=Math.max(z.y,y.y);for(let Y of Q.selectables){let{worldTransform:T}=Y.components;if(T.x>=A&&T.x<=k&&T.y>=F&&T.y<=X)J.addComponent(Y.id,"selected",!0)}}J.commands.removeEntity(N.boxEntityId),N.boxEntityId=null}),$.addSystem("selection-visual").setPriority(j).inPhase("render").inGroup(G).addQuery("selectedUnits",{with:["selected","sprite"]}).setOnEntityEnter("selectedUnits",({entity:Q})=>{Q.components.sprite.tint=W}).addQuery("deselectedUnits",{with:["selectable","sprite"],without:["selected"]}).setOnEntityEnter("deselectedUnits",({entity:Q})=>{Q.components.sprite.tint=16777215})})}export{VJ as createSelectionPlugin,$J as createSelectable};
2
+
3
+ //# debugId=206042F83F56CEF464756E2164756E21
4
+ //# sourceMappingURL=selection.js.map