ecspresso 0.15.0 → 0.16.0

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.
@@ -1,146 +1,148 @@
1
1
  /**
2
2
  * Timer Plugin for ECSpresso
3
3
  *
4
- * Provides ECS-native timers following the "data, not callbacks" philosophy.
5
- * Timers are components processed each frame, automatically cleaned up when entities are removed.
4
+ * ECS-native timers as pure data. An entity may carry multiple named timer
5
+ * slots; the plugin's update system ticks every slot each frame and exposes
6
+ * `justFinished` for the frame a slot crosses its duration. The plugin never
7
+ * touches entity lifecycle — callers despawn (or do anything else) themselves
8
+ * by reacting to `justFinished` or in the slot's `onComplete` callback.
6
9
  */
7
10
  import { type BasePluginOptions } from 'ecspresso';
8
11
  /**
9
- * Data structure passed to onComplete callbacks when a timer completes.
12
+ * Data passed to a slot's `onComplete` callback when its timer completes.
10
13
  *
11
14
  * @example
12
15
  * ```typescript
13
- * createTimer(1.5, {
14
- * onComplete: (data) => {
15
- * console.log(`Timer on entity ${data.entityId} finished after ${data.elapsed}s`);
16
- * }
17
- * });
16
+ * timers: {
17
+ * launch: createTimer(1.5, {
18
+ * onComplete: ({ entityId, slot, elapsed }) => {
19
+ * console.log(`Slot ${slot} on entity ${entityId} finished after ${elapsed}s`);
20
+ * },
21
+ * }),
22
+ * }
18
23
  * ```
19
24
  */
20
25
  export interface TimerEventData {
21
- /** The entity ID that the timer belongs to */
26
+ /** The entity ID that owns the timer slot */
22
27
  entityId: number;
23
- /** The timer's configured duration in seconds */
28
+ /** The slot name within the entity's `timers` map */
29
+ slot: string;
30
+ /** The slot's configured duration in seconds */
24
31
  duration: number;
25
32
  /** The actual elapsed time (may exceed duration slightly) */
26
33
  elapsed: number;
27
34
  }
28
35
  /**
29
- * Timer component data structure.
30
- * Use `justFinished` to detect timer completion in your systems.
36
+ * A single timer's data. Multiple of these can live on one entity, keyed by slot name.
37
+ * Use `justFinished` to detect completion in your systems.
31
38
  */
32
39
  export interface Timer {
33
40
  /** Time accumulated so far (seconds) */
34
41
  elapsed: number;
35
42
  /** Target duration (seconds) */
36
43
  duration: number;
37
- /** Whether timer repeats after completion */
44
+ /** Whether the timer repeats after completion */
38
45
  repeat: boolean;
39
- /** Whether timer is currently running */
46
+ /** Whether the timer is currently running */
40
47
  active: boolean;
41
- /** True for one frame after timer completes */
48
+ /** True for one frame after the timer completes */
42
49
  justFinished: boolean;
43
- /** Optional callback invoked when timer completes */
50
+ /** Optional callback invoked when the timer completes */
44
51
  onComplete?: (data: TimerEventData) => void;
45
52
  }
46
53
  /**
47
54
  * Component types provided by the timer plugin.
48
- * Included automatically via `.withPlugin(createTimerPlugin())`.
55
+ *
56
+ * Each entity carries a single `timers` component whose value is a map of
57
+ * named slots. This lets one entity host independent phase clocks
58
+ * (e.g. `{ launch: ..., shieldDepletion: ..., hangarCycle: ... }`) without
59
+ * one timer's lifecycle constraining another.
49
60
  *
50
61
  * @example
51
62
  * ```typescript
52
63
  * const ecs = ECSpresso.create()
53
64
  * .withPlugin(createTimerPlugin())
54
- * .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()
65
+ * .withComponentTypes<{ fighter: true }>()
55
66
  * .build();
67
+ *
68
+ * ecs.spawn({
69
+ * fighter: true,
70
+ * timers: { launch: createTimer(2.0) },
71
+ * });
56
72
  * ```
57
73
  */
58
74
  export interface TimerComponentTypes {
59
- timer: Timer;
75
+ timers: Record<string, Timer>;
60
76
  }
61
- /**
62
- * Configuration options for the timer plugin.
63
- */
64
77
  export interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {
65
78
  }
66
- /**
67
- * Options for timer creation
68
- */
69
79
  export interface TimerOptions {
70
- /** Callback invoked when timer completes */
80
+ /** Callback invoked when the timer completes */
71
81
  onComplete?: (data: TimerEventData) => void;
72
82
  }
73
83
  /**
74
- * Create a one-shot timer that fires once after the specified duration.
84
+ * Create a one-shot `Timer` to drop into a `timers` slot.
75
85
  *
76
- * @param duration Duration in seconds until the timer completes
77
- * @param options Optional configuration including onComplete callback
78
- * @returns Component object suitable for spreading into spawn()
86
+ * The timer fires `justFinished` for one frame on completion and then idles
87
+ * (`active = false`). The entity is left alone — if the slot's lifetime
88
+ * coincides with the entity's lifetime (vfx, blasts, summon-anim), despawn
89
+ * the host yourself in `onComplete` or in a system that watches `justFinished`.
79
90
  *
80
91
  * @example
81
92
  * ```typescript
82
- * // Timer without callback
83
93
  * ecs.spawn({
84
- * ...createTimer(2),
85
- * explosion: true,
94
+ * fighter: true,
95
+ * timers: { launch: createTimer(2.0) },
86
96
  * });
87
97
  *
88
- * // Timer with onComplete callback
98
+ * // Self-destructing vfx caller owns the despawn:
89
99
  * ecs.spawn({
90
- * ...createTimer(1.5, { onComplete: (data) => console.log('done', data.entityId) }),
100
+ * timers: {
101
+ * fade: createTimer(1.0, {
102
+ * onComplete: ({ entityId }) => ecs.commands.removeEntity(entityId),
103
+ * }),
104
+ * },
91
105
  * });
92
106
  * ```
93
107
  */
94
- export declare function createTimer(duration: number, options?: TimerOptions): Pick<TimerComponentTypes, 'timer'>;
108
+ export declare function createTimer(duration: number, options?: TimerOptions): Timer;
95
109
  /**
96
- * Create a repeating timer that fires every `duration` seconds.
97
- *
98
- * @param duration Duration in seconds between each timer completion
99
- * @param options Optional configuration including onComplete callback
100
- * @returns Component object suitable for spreading into spawn()
110
+ * Create a repeating `Timer` to drop into a `timers` slot. Fires
111
+ * `justFinished` once per cycle and continues running.
101
112
  *
102
113
  * @example
103
114
  * ```typescript
104
- * // Timer without callback
105
115
  * ecs.spawn({
106
- * ...createRepeatingTimer(5),
107
- * spawner: true,
108
- * });
109
- *
110
- * // Repeating timer with onComplete callback
111
- * ecs.spawn({
112
- * ...createRepeatingTimer(3, { onComplete: (data) => console.log('cycle', data.elapsed) }),
116
+ * carrier: true,
117
+ * timers: { hangarCycle: createRepeatingTimer(5.0) },
113
118
  * });
114
119
  * ```
115
120
  */
116
- export declare function createRepeatingTimer(duration: number, options?: TimerOptions): Pick<TimerComponentTypes, 'timer'>;
121
+ export declare function createRepeatingTimer(duration: number, options?: TimerOptions): Timer;
117
122
  /**
118
123
  * Create a timer plugin for ECSpresso.
119
124
  *
120
- * This plugin provides:
121
- * - Timer update system that processes all timer components each frame
122
- * - `justFinished` flag pattern for one-frame completion detection
123
- * - Automatic cleanup when entities are removed
125
+ * The plugin installs one update system that ticks every slot of every
126
+ * `timers` component each frame. It does not touch entity lifecycle
127
+ * react to `justFinished` (or use `onComplete`) and despawn yourself if needed.
124
128
  *
125
129
  * @example
126
130
  * ```typescript
127
- * const ecs = ECSpresso
128
- * .create<Components, Events, Resources>()
131
+ * const ecs = ECSpresso.create()
129
132
  * .withPlugin(createTimerPlugin())
133
+ * .withComponentTypes<{ spawner: true }>()
130
134
  * .build();
131
135
  *
132
- * // Spawn entity with timer
133
136
  * ecs.spawn({
134
- * ...createRepeatingTimer(5),
135
137
  * spawner: true,
138
+ * timers: { wave: createRepeatingTimer(5.0) },
136
139
  * });
137
140
  *
138
- * // React to timer completion in a system
139
141
  * ecs.addSystem('spawn-on-timer')
140
- * .addQuery('spawners', { with: ['timer', 'spawner'] })
141
- * .setProcess((queries, _dt, ecs) => {
142
+ * .addQuery('spawners', { with: ['timers', 'spawner'] })
143
+ * .setProcess(({ queries, ecs }) => {
142
144
  * for (const { components } of queries.spawners) {
143
- * if (components.timer.justFinished) {
145
+ * if (components.timers.wave?.justFinished) {
144
146
  * ecs.spawn({ enemy: true });
145
147
  * }
146
148
  * }
@@ -1,4 +1,4 @@
1
- var J=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(k,{get:(x,A)=>(typeof require<"u"?require:x)[A]}):k)(function(k){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+k+'" is not supported')});import{definePlugin as H}from"ecspresso";function M(k,x){return{timer:{elapsed:0,duration:k,repeat:!1,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function N(k,x){return{timer:{elapsed:0,duration:k,repeat:!0,active:!0,justFinished:!1,onComplete:x?.onComplete}}}function Q(k){let{systemGroup:x="timers",priority:A=0,phase:B="preUpdate"}=k??{};return H("timers").withComponentTypes().withLabels().withGroups().install((C)=>{C.addSystem("timer-update").setPriority(A).inPhase(B).inGroup(x).addQuery("timers",{with:["timer"]}).setProcess(({queries:D,dt:E,ecs:F})=>{for(let z of D.timers){let{timer:j}=z.components;if(j.justFinished=!1,!j.active)continue;if(j.elapsed+=E,j.elapsed<j.duration)continue;if(j.repeat)while(j.elapsed>=j.duration)j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.elapsed-=j.duration;else j.justFinished=!0,j.onComplete?.({entityId:z.id,duration:j.duration,elapsed:j.elapsed}),j.active=!1,F.commands.removeEntity(z.id)}})})}export{Q as createTimerPlugin,M as createTimer,N as createRepeatingTimer};
1
+ var V=((A)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(A,{get:(B,D)=>(typeof require<"u"?require:B)[D]}):A)(function(A){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+A+'" is not supported')});import{definePlugin as U}from"ecspresso";function Y(A,B){return{elapsed:0,duration:A,repeat:!1,active:!0,justFinished:!1,onComplete:B?.onComplete}}function Z(A,B){return{elapsed:0,duration:A,repeat:!0,active:!0,justFinished:!1,onComplete:B?.onComplete}}function _(A){let{systemGroup:B="timers",priority:D=0,phase:L="preUpdate"}=A??{};return U("timers").withComponentTypes().withLabels().withGroups().install((M)=>{M.addSystem("timer-update").setPriority(D).inPhase(L).inGroup(B).addQuery("timers",{with:["timers"]}).setProcess(({queries:N,dt:O})=>{for(let H of N.timers){let K=H.components.timers;for(let J in K){let z=K[J];if(!z)continue;if(z.justFinished=!1,!z.active)continue;if(z.elapsed+=O,z.elapsed<z.duration)continue;if(z.repeat)while(z.elapsed>=z.duration)z.justFinished=!0,z.onComplete?.({entityId:H.id,slot:J,duration:z.duration,elapsed:z.elapsed}),z.elapsed-=z.duration;else z.justFinished=!0,z.onComplete?.({entityId:H.id,slot:J,duration:z.duration,elapsed:z.elapsed}),z.active=!1}}})})}export{_ as createTimerPlugin,Y as createTimer,Z as createRepeatingTimer};
2
2
 
3
- //# debugId=7B8408B3E517C6C664756E2164756E21
3
+ //# debugId=03F5D4A1CC15EDC764756E2164756E21
4
4
  //# sourceMappingURL=timers.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/plugins/scripting/timers.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Timer Plugin for ECSpresso\n *\n * Provides ECS-native timers following the \"data, not callbacks\" philosophy.\n * Timers are components processed each frame, automatically cleaned up when entities are removed.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Event Types ====================\n\n/**\n * Data structure passed to onComplete callbacks when a timer completes.\n *\n * @example\n * ```typescript\n * createTimer(1.5, {\n * onComplete: (data) => {\n * console.log(`Timer on entity ${data.entityId} finished after ${data.elapsed}s`);\n * }\n * });\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that the timer belongs to */\n\tentityId: number;\n\t/** The timer's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n\n/**\n * Timer component data structure.\n * Use `justFinished` to detect timer completion in your systems.\n */\nexport interface Timer {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after timer completes */\n\tjustFinished: boolean;\n\t/** Optional callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Component types provided by the timer plugin.\n * Included automatically via `.withPlugin(createTimerPlugin())`.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ velocity: { x: number; y: number }; player: true }>()\n * .build();\n * ```\n */\nexport interface TimerComponentTypes {\n\ttimer: Timer;\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the timer plugin.\n */\nexport interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Options for timer creation\n */\nexport interface TimerOptions {\n\t/** Callback invoked when timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Create a one-shot timer that fires once after the specified duration.\n *\n * @param duration Duration in seconds until the timer completes\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createTimer(2),\n * explosion: true,\n * });\n *\n * // Timer with onComplete callback\n * ecs.spawn({\n * ...createTimer(1.5, { onComplete: (data) => console.log('done', data.entityId) }),\n * });\n * ```\n */\nexport function createTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: false,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n/**\n * Create a repeating timer that fires every `duration` seconds.\n *\n * @param duration Duration in seconds between each timer completion\n * @param options Optional configuration including onComplete callback\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * // Timer without callback\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // Repeating timer with onComplete callback\n * ecs.spawn({\n * ...createRepeatingTimer(3, { onComplete: (data) => console.log('cycle', data.elapsed) }),\n * });\n * ```\n */\nexport function createRepeatingTimer(\n\tduration: number,\n\toptions?: TimerOptions\n): Pick<TimerComponentTypes, 'timer'> {\n\treturn {\n\t\ttimer: {\n\t\t\telapsed: 0,\n\t\t\tduration,\n\t\t\trepeat: true,\n\t\t\tactive: true,\n\t\t\tjustFinished: false,\n\t\t\tonComplete: options?.onComplete,\n\t\t},\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a timer plugin for ECSpresso.\n *\n * This plugin provides:\n * - Timer update system that processes all timer components each frame\n * - `justFinished` flag pattern for one-frame completion detection\n * - Automatic cleanup when entities are removed\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso\n * .create<Components, Events, Resources>()\n * .withPlugin(createTimerPlugin())\n * .build();\n *\n * // Spawn entity with timer\n * ecs.spawn({\n * ...createRepeatingTimer(5),\n * spawner: true,\n * });\n *\n * // React to timer completion in a system\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timer', 'spawner'] })\n * .setProcess((queries, _dt, ecs) => {\n * for (const { components } of queries.spawners) {\n * if (components.timer.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n */\nexport function createTimerPlugin<G extends string = 'timers'>(\n\toptions?: TimerPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('timers')\n\t\t.withComponentTypes<TimerComponentTypes>()\n\t\t.withLabels<'timer-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('timer-update')\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('timers', {\n\t\t\t\t\twith: ['timer'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs }) => {\n\t\t\t\t\tfor (const entity of queries.timers) {\n\t\t\t\t\t\tconst { timer } = entity.components;\n\n\t\t\t\t\t\t// Reset justFinished flag from previous frame\n\t\t\t\t\t\ttimer.justFinished = false;\n\n\t\t\t\t\t\t// Skip inactive timers\n\t\t\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t\t\t// Accumulate time\n\t\t\t\t\t\ttimer.elapsed += dt;\n\n\t\t\t\t\t\t// Check if timer completed\n\t\t\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t\t\t// Timer completed - handle based on repeat mode\n\t\t\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t\t\t// Handle multiple cycles in one frame\n\t\t\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// One-shot timer\n\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\ttimer.onComplete?.({ entityId: entity.id, duration: timer.duration, elapsed: timer.elapsed });\n\t\t\t\t\t\t\ttimer.active = false;\n\t\t\t\t\t\t\t// Auto-remove one-shot timer entities after completion.\n\t\t\t\t\t\t\t// If configurability is needed in the future, add an autoRemove option to TimerOptions.\n\t\t\t\t\t\t\tecs.commands.removeEntity(entity.id);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
5
+ "/**\n * Timer Plugin for ECSpresso\n *\n * ECS-native timers as pure data. An entity may carry multiple named timer\n * slots; the plugin's update system ticks every slot each frame and exposes\n * `justFinished` for the frame a slot crosses its duration. The plugin never\n * touches entity lifecycle — callers despawn (or do anything else) themselves\n * by reacting to `justFinished` or in the slot's `onComplete` callback.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Event Types ====================\n\n/**\n * Data passed to a slot's `onComplete` callback when its timer completes.\n *\n * @example\n * ```typescript\n * timers: {\n * launch: createTimer(1.5, {\n * onComplete: ({ entityId, slot, elapsed }) => {\n * console.log(`Slot ${slot} on entity ${entityId} finished after ${elapsed}s`);\n * },\n * }),\n * }\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that owns the timer slot */\n\tentityId: number;\n\t/** The slot name within the entity's `timers` map */\n\tslot: string;\n\t/** The slot's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n/**\n * A single timer's data. Multiple of these can live on one entity, keyed by slot name.\n * Use `justFinished` to detect completion in your systems.\n */\nexport interface Timer {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether the timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether the timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after the timer completes */\n\tjustFinished: boolean;\n\t/** Optional callback invoked when the timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Component types provided by the timer plugin.\n *\n * Each entity carries a single `timers` component whose value is a map of\n * named slots. This lets one entity host independent phase clocks\n * (e.g. `{ launch: ..., shieldDepletion: ..., hangarCycle: ... }`) without\n * one timer's lifecycle constraining another.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ fighter: true }>()\n * .build();\n *\n * ecs.spawn({\n * fighter: true,\n * timers: { launch: createTimer(2.0) },\n * });\n * ```\n */\nexport interface TimerComponentTypes {\n\ttimers: Record<string, Timer>;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nexport interface TimerOptions {\n\t/** Callback invoked when the timer completes */\n\tonComplete?: (data: TimerEventData) => void;\n}\n\n/**\n * Create a one-shot `Timer` to drop into a `timers` slot.\n *\n * The timer fires `justFinished` for one frame on completion and then idles\n * (`active = false`). The entity is left alone — if the slot's lifetime\n * coincides with the entity's lifetime (vfx, blasts, summon-anim), despawn\n * the host yourself in `onComplete` or in a system that watches `justFinished`.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * fighter: true,\n * timers: { launch: createTimer(2.0) },\n * });\n *\n * // Self-destructing vfx caller owns the despawn:\n * ecs.spawn({\n * timers: {\n * fade: createTimer(1.0, {\n * onComplete: ({ entityId }) => ecs.commands.removeEntity(entityId),\n * }),\n * },\n * });\n * ```\n */\nexport function createTimer(duration: number, options?: TimerOptions): Timer {\n\treturn {\n\t\telapsed: 0,\n\t\tduration,\n\t\trepeat: false,\n\t\tactive: true,\n\t\tjustFinished: false,\n\t\tonComplete: options?.onComplete,\n\t};\n}\n\n/**\n * Create a repeating `Timer` to drop into a `timers` slot. Fires\n * `justFinished` once per cycle and continues running.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * carrier: true,\n * timers: { hangarCycle: createRepeatingTimer(5.0) },\n * });\n * ```\n */\nexport function createRepeatingTimer(duration: number, options?: TimerOptions): Timer {\n\treturn {\n\t\telapsed: 0,\n\t\tduration,\n\t\trepeat: true,\n\t\tactive: true,\n\t\tjustFinished: false,\n\t\tonComplete: options?.onComplete,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a timer plugin for ECSpresso.\n *\n * The plugin installs one update system that ticks every slot of every\n * `timers` component each frame. It does not touch entity lifecycle —\n * react to `justFinished` (or use `onComplete`) and despawn yourself if needed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ spawner: true }>()\n * .build();\n *\n * ecs.spawn({\n * spawner: true,\n * timers: { wave: createRepeatingTimer(5.0) },\n * });\n *\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timers', 'spawner'] })\n * .setProcess(({ queries, ecs }) => {\n * for (const { components } of queries.spawners) {\n * if (components.timers.wave?.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n */\nexport function createTimerPlugin<G extends string = 'timers'>(\n\toptions?: TimerPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('timers')\n\t\t.withComponentTypes<TimerComponentTypes>()\n\t\t.withLabels<'timer-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('timer-update')\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('timers', { with: ['timers'] })\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.timers) {\n\t\t\t\t\t\tconst slots = entity.components.timers;\n\t\t\t\t\t\tfor (const slot in slots) {\n\t\t\t\t\t\t\tconst timer = slots[slot];\n\t\t\t\t\t\t\tif (!timer) continue;\n\n\t\t\t\t\t\t\ttimer.justFinished = false;\n\t\t\t\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t\t\t\ttimer.elapsed += dt;\n\t\t\t\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\t\ttimer.onComplete?.({\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\t\t\t\tduration: timer.duration,\n\t\t\t\t\t\t\t\t\t\telapsed: timer.elapsed,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\ttimer.onComplete?.({\n\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\t\t\tduration: timer.duration,\n\t\t\t\t\t\t\t\t\telapsed: timer.elapsed,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\ttimer.active = false;\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
6
  ],
7
- "mappings": "2PAOA,uBAAS,kBAqGF,SAAS,CAAW,CAC1B,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAwBM,SAAS,CAAoB,CACnC,EACA,EACqC,CACrC,MAAO,CACN,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,CACD,EAsCM,SAAS,CAA8C,CAC7D,EACC,CACD,IACC,cAAc,SACd,WAAW,EACX,QAAQ,aACL,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAAwC,EACxC,WAA2B,EAC3B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CACnB,KAAM,CAAC,OAAO,CACf,CAAC,EACA,WAAW,EAAG,UAAS,KAAI,SAAU,CACrC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAQ,SAAU,EAAO,WAMzB,GAHA,EAAM,aAAe,GAGjB,CAAC,EAAM,OAAQ,SAMnB,GAHA,EAAM,SAAW,EAGb,EAAM,QAAU,EAAM,SAAU,SAGpC,GAAI,EAAM,OAET,MAAO,EAAM,SAAW,EAAM,SAC7B,EAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,SAAW,EAAM,SAIxB,OAAM,aAAe,GACrB,EAAM,aAAa,CAAE,SAAU,EAAO,GAAI,SAAU,EAAM,SAAU,QAAS,EAAM,OAAQ,CAAC,EAC5F,EAAM,OAAS,GAGf,EAAI,SAAS,aAAa,EAAO,EAAE,GAGrC,EACF",
8
- "debugId": "7B8408B3E517C6C664756E2164756E21",
7
+ "mappings": "2PAUA,uBAAS,kBA+GF,SAAS,CAAW,CAAC,EAAkB,EAA+B,CAC5E,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,EAeM,SAAS,CAAoB,CAAC,EAAkB,EAA+B,CACrF,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,EAmCM,SAAS,CAA8C,CAC7D,EACC,CACD,IACC,cAAc,SACd,WAAW,EACX,QAAQ,aACL,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAAwC,EACxC,WAA2B,EAC3B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CAAE,KAAM,CAAC,QAAQ,CAAE,CAAC,EACvC,WAAW,EAAG,UAAS,QAAS,CAChC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAM,EAAQ,EAAO,WAAW,OAChC,QAAW,KAAQ,EAAO,CACzB,IAAM,EAAQ,EAAM,GACpB,GAAI,CAAC,EAAO,SAGZ,GADA,EAAM,aAAe,GACjB,CAAC,EAAM,OAAQ,SAGnB,GADA,EAAM,SAAW,EACb,EAAM,QAAU,EAAM,SAAU,SAEpC,GAAI,EAAM,OACT,MAAO,EAAM,SAAW,EAAM,SAC7B,EAAM,aAAe,GACrB,EAAM,aAAa,CAClB,SAAU,EAAO,GACjB,OACA,SAAU,EAAM,SAChB,QAAS,EAAM,OAChB,CAAC,EACD,EAAM,SAAW,EAAM,SAGxB,OAAM,aAAe,GACrB,EAAM,aAAa,CAClB,SAAU,EAAO,GACjB,OACA,SAAU,EAAM,SAChB,QAAS,EAAM,OAChB,CAAC,EACD,EAAM,OAAS,KAIlB,EACF",
8
+ "debugId": "03F5D4A1CC15EDC764756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -195,8 +195,14 @@ export declare class SystemBuilder<Cfg extends WorldConfig = EmptyConfig, Querie
195
195
  */
196
196
  setOnDetach(onDetach: LifecycleFunction<Cfg>): this;
197
197
  /**
198
- * Set the onInitialize lifecycle hook
199
- * Called when the system is initialized via ECSpresso.initialize() method
198
+ * Set the onInitialize lifecycle hook.
199
+ *
200
+ * Fires exactly once per system. For systems added before `initialize()`,
201
+ * the hook is awaited inside `initialize()` itself. For systems added
202
+ * after `initialize()` has returned, the hook fires on registration (at
203
+ * the next `update()`'s finalize step) — async hooks run fire-and-forget,
204
+ * so don't rely on completion ordering against the first `process` call.
205
+ *
200
206
  * @param onInitialize Function to run when this system is initialized
201
207
  * @returns This SystemBuilder instance for method chaining
202
208
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecspresso",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",