@phalanx-engine/abilities 0.1.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.
Files changed (152) hide show
  1. package/README.md +564 -0
  2. package/dist/api/AbilitySystemFacade.d.ts +34 -0
  3. package/dist/api/AbilitySystemFacade.d.ts.map +1 -0
  4. package/dist/api/AbilitySystemFacade.js +213 -0
  5. package/dist/api/createAbilitySystem.d.ts +49 -0
  6. package/dist/api/createAbilitySystem.d.ts.map +1 -0
  7. package/dist/api/createAbilitySystem.js +205 -0
  8. package/dist/api/defineAbility.d.ts +3 -0
  9. package/dist/api/defineAbility.d.ts.map +1 -0
  10. package/dist/api/defineAbility.js +9 -0
  11. package/dist/api/defineAbilitySystem.d.ts +8 -0
  12. package/dist/api/defineAbilitySystem.d.ts.map +1 -0
  13. package/dist/api/defineAbilitySystem.js +7 -0
  14. package/dist/api/defineAttribute.d.ts +3 -0
  15. package/dist/api/defineAttribute.d.ts.map +1 -0
  16. package/dist/api/defineAttribute.js +3 -0
  17. package/dist/api/defineEffect.d.ts +6 -0
  18. package/dist/api/defineEffect.d.ts.map +1 -0
  19. package/dist/api/defineEffect.js +23 -0
  20. package/dist/api/index.d.ts +11 -0
  21. package/dist/api/index.d.ts.map +1 -0
  22. package/dist/api/index.js +6 -0
  23. package/dist/components/AbilitiesComponentType.d.ts +7 -0
  24. package/dist/components/AbilitiesComponentType.d.ts.map +1 -0
  25. package/dist/components/AbilitiesComponentType.js +7 -0
  26. package/dist/components/AbilitySystemComponent.d.ts +17 -0
  27. package/dist/components/AbilitySystemComponent.d.ts.map +1 -0
  28. package/dist/components/AbilitySystemComponent.js +31 -0
  29. package/dist/components/ActiveEffectsComponent.d.ts +12 -0
  30. package/dist/components/ActiveEffectsComponent.d.ts.map +1 -0
  31. package/dist/components/ActiveEffectsComponent.js +6 -0
  32. package/dist/components/AttributesComponent.d.ts +9 -0
  33. package/dist/components/AttributesComponent.d.ts.map +1 -0
  34. package/dist/components/AttributesComponent.js +15 -0
  35. package/dist/components/GameplayTagsComponent.d.ts +8 -0
  36. package/dist/components/GameplayTagsComponent.d.ts.map +1 -0
  37. package/dist/components/GameplayTagsComponent.js +7 -0
  38. package/dist/components/index.d.ts +7 -0
  39. package/dist/components/index.d.ts.map +1 -0
  40. package/dist/components/index.js +5 -0
  41. package/dist/cues/Cue.d.ts +15 -0
  42. package/dist/cues/Cue.d.ts.map +1 -0
  43. package/dist/cues/Cue.js +7 -0
  44. package/dist/cues/index.d.ts +3 -0
  45. package/dist/cues/index.d.ts.map +1 -0
  46. package/dist/cues/index.js +1 -0
  47. package/dist/events/AbilityEvents.d.ts +10 -0
  48. package/dist/events/AbilityEvents.d.ts.map +1 -0
  49. package/dist/events/AbilityEvents.js +1 -0
  50. package/dist/events/CueEvents.d.ts +5 -0
  51. package/dist/events/CueEvents.d.ts.map +1 -0
  52. package/dist/events/CueEvents.js +4 -0
  53. package/dist/events/index.d.ts +5 -0
  54. package/dist/events/index.d.ts.map +1 -0
  55. package/dist/events/index.js +2 -0
  56. package/dist/index.d.ts +10 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +9 -0
  59. package/dist/registry/AbilityHooksRegistry.d.ts +10 -0
  60. package/dist/registry/AbilityHooksRegistry.d.ts.map +1 -0
  61. package/dist/registry/AbilityHooksRegistry.js +25 -0
  62. package/dist/registry/AbilityRegistry.d.ts +6 -0
  63. package/dist/registry/AbilityRegistry.d.ts.map +1 -0
  64. package/dist/registry/AbilityRegistry.js +4 -0
  65. package/dist/registry/AbilitySystemRegistries.d.ts +12 -0
  66. package/dist/registry/AbilitySystemRegistries.d.ts.map +1 -0
  67. package/dist/registry/AbilitySystemRegistries.js +12 -0
  68. package/dist/registry/AttributeRegistry.d.ts +7 -0
  69. package/dist/registry/AttributeRegistry.d.ts.map +1 -0
  70. package/dist/registry/AttributeRegistry.js +11 -0
  71. package/dist/registry/DefinitionRegistry.d.ts +16 -0
  72. package/dist/registry/DefinitionRegistry.d.ts.map +1 -0
  73. package/dist/registry/DefinitionRegistry.js +37 -0
  74. package/dist/registry/EffectRegistry.d.ts +6 -0
  75. package/dist/registry/EffectRegistry.d.ts.map +1 -0
  76. package/dist/registry/EffectRegistry.js +4 -0
  77. package/dist/registry/index.d.ts +7 -0
  78. package/dist/registry/index.d.ts.map +1 -0
  79. package/dist/registry/index.js +5 -0
  80. package/dist/runtime/AbilitySystemRuntime.d.ts +26 -0
  81. package/dist/runtime/AbilitySystemRuntime.d.ts.map +1 -0
  82. package/dist/runtime/AbilitySystemRuntime.js +11 -0
  83. package/dist/runtime/GameplayCueBuffer.d.ts +10 -0
  84. package/dist/runtime/GameplayCueBuffer.d.ts.map +1 -0
  85. package/dist/runtime/GameplayCueBuffer.js +16 -0
  86. package/dist/runtime/InstanceIdCounter.d.ts +6 -0
  87. package/dist/runtime/InstanceIdCounter.d.ts.map +1 -0
  88. package/dist/runtime/InstanceIdCounter.js +10 -0
  89. package/dist/runtime/index.d.ts +6 -0
  90. package/dist/runtime/index.d.ts.map +1 -0
  91. package/dist/runtime/index.js +3 -0
  92. package/dist/systems/AbilityActivationSystem.d.ts +23 -0
  93. package/dist/systems/AbilityActivationSystem.d.ts.map +1 -0
  94. package/dist/systems/AbilityActivationSystem.js +277 -0
  95. package/dist/systems/AbilityHookExecutorSystem.d.ts +10 -0
  96. package/dist/systems/AbilityHookExecutorSystem.d.ts.map +1 -0
  97. package/dist/systems/AbilityHookExecutorSystem.js +35 -0
  98. package/dist/systems/AttributeAggregationSystem.d.ts +12 -0
  99. package/dist/systems/AttributeAggregationSystem.d.ts.map +1 -0
  100. package/dist/systems/AttributeAggregationSystem.js +117 -0
  101. package/dist/systems/CueBufferCleanupSystem.d.ts +8 -0
  102. package/dist/systems/CueBufferCleanupSystem.d.ts.map +1 -0
  103. package/dist/systems/CueBufferCleanupSystem.js +11 -0
  104. package/dist/systems/CueDispatchSystem.d.ts +8 -0
  105. package/dist/systems/CueDispatchSystem.d.ts.map +1 -0
  106. package/dist/systems/CueDispatchSystem.js +17 -0
  107. package/dist/systems/CuePresentationSystem.d.ts +14 -0
  108. package/dist/systems/CuePresentationSystem.d.ts.map +1 -0
  109. package/dist/systems/CuePresentationSystem.js +50 -0
  110. package/dist/systems/EffectApplicationSystem.d.ts +20 -0
  111. package/dist/systems/EffectApplicationSystem.d.ts.map +1 -0
  112. package/dist/systems/EffectApplicationSystem.js +174 -0
  113. package/dist/systems/EffectTickSystem.d.ts +16 -0
  114. package/dist/systems/EffectTickSystem.d.ts.map +1 -0
  115. package/dist/systems/EffectTickSystem.js +152 -0
  116. package/dist/systems/index.d.ts +9 -0
  117. package/dist/systems/index.d.ts.map +1 -0
  118. package/dist/systems/index.js +8 -0
  119. package/dist/targeting/TargetResolver.d.ts +19 -0
  120. package/dist/targeting/TargetResolver.d.ts.map +1 -0
  121. package/dist/targeting/TargetResolver.js +45 -0
  122. package/dist/targeting/index.d.ts +3 -0
  123. package/dist/targeting/index.d.ts.map +1 -0
  124. package/dist/targeting/index.js +1 -0
  125. package/dist/types/AbilityDef.d.ts +13 -0
  126. package/dist/types/AbilityDef.d.ts.map +1 -0
  127. package/dist/types/AbilityDef.js +1 -0
  128. package/dist/types/AbilityHook.d.ts +10 -0
  129. package/dist/types/AbilityHook.d.ts.map +1 -0
  130. package/dist/types/AbilityHook.js +1 -0
  131. package/dist/types/ActiveEffectInstance.d.ts +9 -0
  132. package/dist/types/ActiveEffectInstance.d.ts.map +1 -0
  133. package/dist/types/ActiveEffectInstance.js +1 -0
  134. package/dist/types/AttributeDef.d.ts +10 -0
  135. package/dist/types/AttributeDef.d.ts.map +1 -0
  136. package/dist/types/AttributeDef.js +1 -0
  137. package/dist/types/CueEvent.d.ts +9 -0
  138. package/dist/types/CueEvent.d.ts.map +1 -0
  139. package/dist/types/CueEvent.js +1 -0
  140. package/dist/types/EffectDef.d.ts +23 -0
  141. package/dist/types/EffectDef.d.ts.map +1 -0
  142. package/dist/types/EffectDef.js +17 -0
  143. package/dist/types/ModifierOp.d.ts +8 -0
  144. package/dist/types/ModifierOp.d.ts.map +1 -0
  145. package/dist/types/ModifierOp.js +1 -0
  146. package/dist/types/TargetSpec.d.ts +32 -0
  147. package/dist/types/TargetSpec.d.ts.map +1 -0
  148. package/dist/types/TargetSpec.js +1 -0
  149. package/dist/types/index.d.ts +10 -0
  150. package/dist/types/index.d.ts.map +1 -0
  151. package/dist/types/index.js +1 -0
  152. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,564 @@
1
+ # Phalanx Abilities
2
+
3
+ A deterministic gameplay ability system (GAS-inspired) for the [Phalanx Engine](../README.md). Built for lockstep multiplayer: attributes, effects, tags, abilities, targeting, and gameplay cues all run on fixed-point math and tick-aligned ECS systems.
4
+
5
+ > Sibling packages: [phalanx-ecs](../phalanx-ecs/README.md) (ECS core), [phalanx-math](../phalanx-math/README.md) (fixed-point math).
6
+
7
+ ## Features
8
+
9
+ - **Attributes**: `base` + `current`, per-definition `min`/`max`, FIFO modifier aggregation (`Add` / `Multiply` / `Override`), clamping
10
+ - **Gameplay effects**: `Instant`, `Duration`, `Periodic` with tick-based `durationTicks` / `periodTicks`
11
+ - **Gameplay tags**: hierarchical strings (`State.Buff.Speed`), `tagsRequired` / `tagsBlocked` / `tagsGranted` on effects and abilities
12
+ - **Abilities**: declarative definitions, activation queue, cost/cooldown via effects, `CanActivate` checks
13
+ - **Targeting**: `Self`, `Entity`, `Point` with deterministic resolve
14
+ - **Activation hooks**: deterministic callbacks for projectiles and rockets (user-owned entities)
15
+ - **Gameplay cues**: per-tick simulation buffer → optional client presentation via self-managing `Cue` instances (VFX/SFX/UI)
16
+
17
+ ### MVP scope
18
+
19
+ Included in v0.1: flat modifiers, channeling via `Duration` + `removeEffectsByTag`, hooks.
20
+
21
+ Planned for v2: execution calculations, granted abilities, stacking rules, line-of-sight raycast, SoA attribute storage.
22
+
23
+ ## Installation
24
+
25
+ > Not on npm yet — clone the monorepo and build via pnpm.
26
+
27
+ ```bash
28
+ git clone https://github.com/phaeton-forge/phalanx-engine.git
29
+ cd phalanx-engine
30
+ pnpm install
31
+ pnpm --filter @phalanx-engine/abilities build
32
+ ```
33
+
34
+ **Peer dependencies:** `@phalanx-engine/ecs` ^0.1.0, `@phalanx-engine/math` ^0.1.0
35
+
36
+ ## Quick start
37
+
38
+ This example wires **@phalanx-engine/ecs** (`GameWorld`) and **@phalanx-engine/math** (`FP`) into one abilities pipeline.
39
+
40
+ ```typescript
41
+ import { Entity, GameWorld, resetEntityIdCounter } from '@phalanx-engine/ecs';
42
+ import { FP } from '@phalanx-engine/math';
43
+ import {
44
+ createAbilitySystem,
45
+ Cue,
46
+ defineAbility,
47
+ defineAbilitySystem,
48
+ defineAttribute,
49
+ defineEffect,
50
+ type CueContext,
51
+ type GameplayCueDispatchedEvent,
52
+ } from '@phalanx-engine/abilities';
53
+
54
+ /** Presentation cue — spawned per dispatch, animated in afterFrame. */
55
+ class FireballHitCue extends Cue {
56
+ private done = false;
57
+
58
+ public onSpawn(event: GameplayCueDispatchedEvent, _ctx: CueContext): void {
59
+ // build VFX from event + ctx.entityManager
60
+ }
61
+
62
+ public override update(_dt: number): void {
63
+ // animate each render frame
64
+ if (/* animation complete */) this.done = true;
65
+ }
66
+
67
+ public override isFinished(): boolean {
68
+ return this.done;
69
+ }
70
+
71
+ public override dispose(): void {
72
+ // remove VFX / free resources
73
+ }
74
+ }
75
+
76
+ // 1. Declare attributes, effects, and abilities once (typically a dedicated module).
77
+ const combatDefinitions = defineAbilitySystem({
78
+ attributes: [
79
+ defineAttribute({
80
+ id: 'Health',
81
+ default: FP.FromInt(100),
82
+ min: FP.FromInt(0),
83
+ max: FP.FromInt(100),
84
+ clamp: 'both',
85
+ }),
86
+ defineAttribute({
87
+ id: 'Mana',
88
+ default: FP.FromInt(50),
89
+ min: FP.FromInt(0),
90
+ max: FP.FromInt(50),
91
+ clamp: 'both',
92
+ }),
93
+ ],
94
+ effects: [
95
+ defineEffect({
96
+ id: 'Effect.Fireball',
97
+ type: 'Instant',
98
+ modifiers: [{ attributeId: 'Health', op: 'Add', magnitude: FP.FromInt(-25) }],
99
+ cues: ['Cue.Fireball.Hit'],
100
+ }),
101
+ defineEffect({
102
+ id: 'Effect.Fireball.Cooldown',
103
+ type: 'Duration',
104
+ durationTicks: 30,
105
+ tagsGranted: ['Cooldown.Ability.Fireball'],
106
+ }),
107
+ ],
108
+ abilities: [
109
+ defineAbility({
110
+ id: 'Ability.Fireball',
111
+ costEffectId: undefined,
112
+ cooldownEffectId: 'Effect.Fireball.Cooldown',
113
+ activationBlockedTags: ['Cooldown.Ability.Fireball', 'State.Stun'],
114
+ target: { kind: 'Entity', origin: { kind: 'Caller' } },
115
+ targetEffectIds: ['Effect.Fireball'],
116
+ }),
117
+ ],
118
+ });
119
+
120
+ resetEntityIdCounter();
121
+
122
+ const world = new GameWorld({ tickRate: 20 });
123
+
124
+ // 2. Create the ability system and register tick systems on the world.
125
+ const abilities = createAbilitySystem(world, {
126
+ definitions: combatDefinitions,
127
+ cues: {
128
+ 'Cue.Fireball.Hit': () => new FireballHitCue(),
129
+ },
130
+ });
131
+
132
+ world.registerSystems([...abilities.tickSystems], [], 'default');
133
+
134
+ // 3. Spawn a combatant with the bundled AbilitySystemComponent.
135
+ const hero = new Entity();
136
+ const abilityComponent = abilities.initComponent({
137
+ attributes: { Health: FP.FromInt(100), Mana: FP.FromInt(50) },
138
+ abilities: ['Ability.Fireball'],
139
+ tags: ['Team.Hero'],
140
+ });
141
+ hero.addComponent(abilityComponent);
142
+ world.entityManager.addEntity(hero);
143
+
144
+ // 4. Drive simulation by tick (lockstep-safe).
145
+ abilities.activateAbility(hero.id, 'Ability.Fireball', { entityId: enemyId });
146
+ world.processAllTicks(currentTick);
147
+ // CuePresentationSystem runs in afterFrame — call world.start() or invoke
148
+ // afterFrame on frame systems when driving presentation manually in tests.
149
+ ```
150
+
151
+ **Tick discipline:** `activateAbility` and `applyEffect` enqueue work. Observable changes (attributes, tags, active effects) apply when ability **tick systems** run inside `world.processAllTicks()` (or your lockstep `beforeTick`/`afterTick` pipeline). Never expect synchronous attribute updates in the same call stack as the facade.
152
+
153
+ ## Architecture
154
+
155
+ ```
156
+ createAbilitySystem(world, config)
157
+ ├── AbilitySystemRegistries (per-world: attributes, effects, abilities, hooks)
158
+ ├── AbilitySystemRuntime (activation queue, instance ids, GameplayCueBuffer)
159
+ ├── AbilitySystemFacade (applyEffect, activateAbility, …)
160
+ └── tickSystems[] (registered on GameWorld)
161
+
162
+ Per simulation tick (client GameWorld):
163
+ AbilityActivationSystem → CanActivate, cost/cooldown/self effects
164
+ EffectApplicationSystem → pendingAdd, Instant/Duration/Periodic, tags, OnApplied cues
165
+ AbilityHookExecutorSystem → hookId callbacks (projectiles, rockets)
166
+ EffectTickSystem → duration countdown, Periodic ticks, OnExpired cues
167
+ AttributeAggregationSystem → FIFO modifiers + clamp → current
168
+ CueDispatchSystem? → CuePresentationSystem (when cues map is non-empty)
169
+ CueBufferCleanupSystem → clear buffer end of tick (effects/full pipelines)
170
+
171
+ Per render frame (client GameWorld):
172
+ CuePresentationSystem? → afterFrame: spawn Cue per dispatch, update(dt), dispose
173
+ ```
174
+
175
+ Registries and runtime state are **per `GameWorld`**, not global singletons. Two worlds do not share attribute indices or cue buffers.
176
+
177
+ Gameplay cues exist only where deterministic simulation runs (typically each **client** `GameWorld`). A headless relay server does not need the cue pipeline.
178
+
179
+ ## Core concepts
180
+
181
+ ### Attributes
182
+
183
+ Registered with `defineAttribute`. Each entity with an `AbilitySystemComponent` holds `base` and `current` in `BigInt64Array` slots indexed by registration order.
184
+
185
+ Modifier aggregation (FIFO by `instanceId`):
186
+
187
+ ```
188
+ acc = base
189
+ for each active effect instance (sorted by instanceId ASC):
190
+ Add → acc = acc + magnitude
191
+ Multiply → acc = acc * magnitude
192
+ Override → acc = magnitude
193
+ current = clamp(acc) per AttributeDef
194
+ ```
195
+
196
+ ### Effects
197
+
198
+ | Type | Behavior |
199
+ |------|----------|
200
+ | `Instant` | Modifies `base` immediately when applied |
201
+ | `Duration` | Stays in `ActiveEffectsComponent.queue` for `durationTicks`; grants tags while active |
202
+ | `Periodic` | Duration + fires modifiers every `periodTicks`; optional `executePeriodicOnApplication` |
203
+
204
+ Durations and periods are **whole simulation ticks** (`number`), compared to `runtime.currentTick` — not `FixedPoint` values.
205
+
206
+ ### Abilities
207
+
208
+ `defineAbility` describes activation rules. On success, the activation system applies `costEffectId`, `cooldownEffectId`, and `selfEffectIds` to the caster, resolves `target`, applies `targetEffectIds`, then runs `hookId` if set.
209
+
210
+ `activateAbility` returns `true` when the request is **queued**, not when it ultimately succeeds. Listen for `ABILITY_ACTIVATED_EVENT` on the world `EventBus` for the final verdict and resolved targets.
211
+
212
+ ### Gameplay tags
213
+
214
+ - **Effect-granted**: from `tagsGranted`; removed when the effect expires or is stripped via `removeEffectsByTag`
215
+ - **Ad-hoc**: `abilities.addTag` / `removeTag` for spawn setup (teams, factions)
216
+ - **Ability gates**: `tagsRequired`, `activationBlockedTags` on `AbilityDef`; effect `tagsRequired` / `tagsBlocked` on targets
217
+
218
+ ### Targeting
219
+
220
+ ```typescript
221
+ type TargetSpec =
222
+ | { kind: 'Self' }
223
+ | { kind: 'Entity'; origin: TargetOrigin }
224
+ | { kind: 'Point'; origin: TargetOrigin };
225
+
226
+ type TargetOrigin =
227
+ | { kind: 'Caster' }
228
+ | { kind: 'TargetEntity'; entityId: number }
229
+ | { kind: 'Point'; x: FixedPoint; z: FixedPoint }
230
+ | { kind: 'Caller' }; // reads activateAbility(..., providedTarget)
231
+ ```
232
+
233
+ ### Activation hooks
234
+
235
+ Register in `createAbilitySystem({ hooks: { ... } })` or `facade.registerHook`. Hooks run **after** cost/cooldown/self effects on the activation tick. Use for spawning projectiles/rockets (entities live in **your** game code, not in this package).
236
+
237
+ ```typescript
238
+ import type { AbilityHook } from '@phalanx-engine/abilities';
239
+
240
+ const spawnProjectile: AbilityHook = (ctx) => {
241
+ // ctx.abilityId, ctx.casterEntityId, ctx.resolvedTargets, ctx.providedTarget, ctx.tick
242
+ // Spawn projectile entity; on hit call abilities.applyEffect(targetId, 'Effect.Damage', casterId)
243
+ };
244
+ ```
245
+
246
+ ## Gameplay ability recipes
247
+
248
+ These five patterns match the arena-shooter target game. Projectiles and damage multipliers outside pure GAS math are called out explicitly.
249
+
250
+ ### 1. Auto-attack (projectile on hit)
251
+
252
+ Ability applies cooldown; hook spawns the projectile; damage is an **Instant** effect on impact.
253
+
254
+ ```typescript
255
+ defineEffect({
256
+ id: 'Effect.AutoAttack.Cooldown',
257
+ type: 'Duration',
258
+ durationTicks: 30,
259
+ tagsGranted: ['Cooldown.Ability.AutoAttack'],
260
+ });
261
+ defineEffect({
262
+ id: 'Effect.AutoAttack.Damage',
263
+ type: 'Instant',
264
+ modifiers: [{ attributeId: 'Health', op: 'Add', magnitude: FP.FromInt(-10) }],
265
+ cues: ['Cue.AutoAttack.Hit'],
266
+ });
267
+ defineAbility({
268
+ id: 'Ability.AutoAttack',
269
+ cooldownEffectId: 'Effect.AutoAttack.Cooldown',
270
+ activationBlockedTags: ['Cooldown.Ability.AutoAttack', 'State.Stun'],
271
+ target: { kind: 'Entity', origin: { kind: 'Caller' } },
272
+ hookId: 'Hook.SpawnProjectile.AutoAttack',
273
+ });
274
+
275
+ // In hooks['Hook.SpawnProjectile.AutoAttack']: spawn projectile entity.
276
+ // On hit: abilities.applyEffect(targetId, 'Effect.AutoAttack.Damage', casterId);
277
+ ```
278
+
279
+ ### 2. Health regeneration (Periodic Effect)
280
+
281
+ ```typescript
282
+ defineEffect({
283
+ id: 'Effect.HealthRegen',
284
+ type: 'Periodic',
285
+ periodTicks: 60,
286
+ durationTicks: 600,
287
+ modifiers: [{ attributeId: 'Health', op: 'Add', magnitude: FP.FromInt(5) }],
288
+ });
289
+ defineAbility({
290
+ id: 'Ability.HealthRegen',
291
+ target: { kind: 'Self' },
292
+ targetEffectIds: ['Effect.HealthRegen'],
293
+ });
294
+ ```
295
+
296
+ ### 3. Armor-shred beam (channeling)
297
+
298
+ ```typescript
299
+ defineEffect({
300
+ id: 'Effect.ArmorShred',
301
+ type: 'Duration',
302
+ durationTicks: 300,
303
+ modifiers: [{ attributeId: 'Armor', op: 'Add', magnitude: FP.FromInt(-20) }],
304
+ tagsGranted: ['State.Debuff.ArmorShred'],
305
+ });
306
+ defineAbility({
307
+ id: 'Ability.ArmorShredBeam',
308
+ target: { kind: 'Entity', origin: { kind: 'Caller' } },
309
+ targetEffectIds: ['Effect.ArmorShred'],
310
+ });
311
+
312
+ // On beam release (input system, same tick discipline):
313
+ abilities.removeEffectsByTag(targetId, 'State.Debuff.ArmorShred');
314
+ ```
315
+
316
+ ### 4. Mark beam (damage multiplier — user-side)
317
+
318
+ The library stores `IncomingDamageMultiplier` on the target; **your** damage pipeline must read it when applying damage.
319
+
320
+ ```typescript
321
+ defineAttribute({
322
+ id: 'IncomingDamageMultiplier',
323
+ default: FP.FromInt(1),
324
+ min: FP.FromInt(0),
325
+ max: FP.FromInt(10),
326
+ clamp: 'both',
327
+ });
328
+ defineEffect({
329
+ id: 'Effect.Marked',
330
+ type: 'Duration',
331
+ durationTicks: 240,
332
+ modifiers: [
333
+ { attributeId: 'IncomingDamageMultiplier', op: 'Multiply', magnitude: FP.FromFloat(1.25) },
334
+ ],
335
+ tagsGranted: ['State.Marked'],
336
+ });
337
+
338
+ function applyDamageWithMark(
339
+ abilities: AbilitySystem,
340
+ targetId: number,
341
+ baseDamage: FixedPoint
342
+ ): void {
343
+ const mult =
344
+ abilities.tryGetAttribute(targetId, 'IncomingDamageMultiplier')?.current ?? FP.FromInt(1);
345
+ const scaled = FP.Mul(baseDamage, mult);
346
+ // apply Instant damage effect or direct Health modifier with `scaled`
347
+ }
348
+ ```
349
+
350
+ This is an intentional MVP limitation — v2 may add execution calculations.
351
+
352
+ ### 5. Custom execution formulas
353
+
354
+ `phalanx-abilities` doesn't support complex math inside effect definitions yet. For complex formulas (e.g. `Damage = (Base + Strength * 2) * (1 - Armor / 100)`), read the attributes from the facade and calculate manually before calling `applyEffect`.
355
+
356
+ ```typescript
357
+ defineEffect({
358
+ id: 'Effect.Damage.Marked',
359
+ type: 'Instant',
360
+ modifiers: [{ attributeId: 'Health', op: 'Add', magnitude: FP.FromInt(-10) }],
361
+ cues: ['Cue.Hit'],
362
+ });
363
+
364
+ const targetMarked = abilities.hasTag(enemyId, 'State.Marked');
365
+ if (targetMarked) {
366
+ abilities.applyEffect(enemyId, 'Effect.Damage.Marked');
367
+ }
368
+ ```
369
+
370
+ ## Gameplay cues
371
+
372
+ Cues are deterministic simulation-side notifications for local presentation (VFX, SFX, UI). They are **not** networked. Simulation systems write cue events into an internal per-tick buffer; when you register client presentation, the engine dispatches each event and spawns a short-lived `Cue` instance to animate it.
373
+
374
+ ### Simulation pipeline
375
+
376
+ ```text
377
+ simulation systems → GameplayCueBuffer → CueDispatchSystem → CuePresentationSystem
378
+ → CueBufferCleanupSystem (end of tick)
379
+ ```
380
+
381
+ `GameplayCueBuffer` is internal runtime state on `AbilitySystemRuntime` — not a public config option and not an entity component. For tests, use `pipeline: 'effects-retain-cues'` to keep buffered events across ticks without dispatch.
382
+
383
+ Effects declare cues as a shortcut array (OnApplied only) or structured phases:
384
+
385
+ ```typescript
386
+ defineEffect({
387
+ id: 'Effect.Poison',
388
+ type: 'Periodic',
389
+ durationTicks: 6,
390
+ periodTicks: 2,
391
+ cues: {
392
+ onApplied: ['Cue.Poison.Apply'],
393
+ onPeriodic: ['Cue.Poison.Tick'],
394
+ onExpired: ['Cue.Poison.Expire'],
395
+ },
396
+ });
397
+ ```
398
+
399
+ ### Per-dispatch self-managing `Cue` model
400
+
401
+ **One dispatched cue event = one short-lived `Cue` instance.** Register factories in `createAbilitySystem`:
402
+
403
+ ```typescript
404
+ import { Cue, type CueConfig, type CueContext, type GameplayCueDispatchedEvent } from '@phalanx-engine/abilities';
405
+
406
+ const cues: CueConfig = {
407
+ 'Cue.Damage.Sphere': () => new DamageSphereCue(scene),
408
+ 'Cue.Death': () => new DeathCue(scene),
409
+ };
410
+
411
+ createAbilitySystem(world, { definitions, cues });
412
+ ```
413
+
414
+ | Type | Role |
415
+ |------|------|
416
+ | `Cue` | Abstract base: `onSpawn`, `update(dt)`, `isFinished()`, `dispose()` |
417
+ | `CueContext` | Read-only `{ entityManager, eventBus }` — no `GameWorld` |
418
+ | `CueFactory` | `() => Cue` — invoked **per dispatch**, not once at init |
419
+ | `CueConfig` | `Readonly<Record<string, CueFactory>>` — map key is the cue id |
420
+
421
+ **Two-phase init:** the factory closure captures presentation deps (scene, audio); `onSpawn(event, ctx)` binds the instance to the dispatch event and world services.
422
+
423
+ ```typescript
424
+ export class DamageSphereCue extends Cue {
425
+ private done = false;
426
+
427
+ public constructor(private readonly scene: THREE.Scene) {
428
+ super();
429
+ }
430
+
431
+ public onSpawn(event: GameplayCueDispatchedEvent, ctx: CueContext): void {
432
+ const impact = resolveImpact(ctx.entityManager, event);
433
+ if (!impact) {
434
+ this.done = true; // nothing to show — engine skips the active list
435
+ return;
436
+ }
437
+ this.scene.add(createBurstVfx(impact));
438
+ }
439
+
440
+ public override update(dt: number): void {
441
+ // animate each render frame
442
+ if (/* animation complete */) this.done = true;
443
+ }
444
+
445
+ public override isFinished(): boolean {
446
+ return this.done;
447
+ }
448
+
449
+ public override dispose(): void {
450
+ // remove VFX / free resources
451
+ }
452
+ }
453
+ ```
454
+
455
+ **Lifecycle** (driven by `CuePresentationSystem`):
456
+
457
+ 1. Engine subscribes once per `cueId` to `gameplayCueKey(cueId)` on init.
458
+ 2. On each dispatch: `factory()` → `onSpawn(event, ctx)` → active list if not `isFinished()`.
459
+ 3. Each `afterFrame`: `update(dt)`; when `isFinished()`, `dispose()` and remove.
460
+ 4. On world dispose: dispose all live cues.
461
+
462
+ Cues are **presentation-only** — never call `applyEffect` / `activateAbility` from cue code. `phalanx-abilities` does not import rendering libraries; inject scene/audio via the factory closure.
463
+
464
+ A non-empty `cues` map automatically registers `CueDispatchSystem` and `CuePresentationSystem`. Omit `cues` (or pass `{}`) for headless/simulation-only worlds; `CueBufferCleanupSystem` still runs where the pipeline requires it.
465
+
466
+ ### Pipeline: `effects-retain-cues`
467
+
468
+ `pipeline: 'effects-retain-cues'` runs effect systems but **does not** clear the cue buffer each tick — useful for asserting buffered events in tests. It does not register dispatch or presentation unless you also pass a non-empty `cues` map. Using it with an empty `cues` map intentionally retains the buffer without dispatch (dev warning in non-production builds).
469
+
470
+ ## Determinism rules
471
+
472
+ - Use `FP.*` from `phalanx-math` for all modifier magnitudes
473
+ - Store durations as integer **ticks**, not floats or `Date.now`
474
+ - Target resolution is snapshotted at activation; movement after activation does not change which entity or point was targeted
475
+ - Call `resetEntityIdCounter()` from `phalanx-ecs` at match start so projectile spawns get identical ids on every peer
476
+ - Hooks must be pure deterministic simulation — no `Math.random()` or wall-clock time
477
+
478
+ ## API reference
479
+
480
+ ### Factory
481
+
482
+ ```typescript
483
+ createAbilitySystem(world: GameWorld, config: CreateAbilitySystemConfig): AbilitySystem
484
+ ```
485
+
486
+ | Config field | Purpose |
487
+ |--------------|---------|
488
+ | `definitions` | `defineAbilitySystem({ attributes, effects?, abilities? })` |
489
+ | `hooks` | `Record<hookId, AbilityHook>` |
490
+ | `pipeline` | `'full'` (default), `'activation'`, `'effects'`, `'effects-retain-cues'`, `'attributes'` |
491
+ | `cues` | `CueConfig` — `cueId → () => Cue`. Non-empty map registers dispatch + presentation |
492
+
493
+ ### `AbilitySystem` (returned by factory)
494
+
495
+ | Method | Description |
496
+ |--------|-------------|
497
+ | `initComponent(init?)` | Create `AbilitySystemComponent` with optional seed data |
498
+ | `activateAbility(casterId, abilityId, providedTarget?)` | Queue activation |
499
+ | `applyEffect(targetId, effectId, sourceId?)` | Queue effect (`sourceId` defaults to `-1`) |
500
+ | `getAttribute` / `tryGetAttribute` | Read base/current |
501
+ | `hasTag` / `addTag` / `removeTag` | Tag queries and ad-hoc tags |
502
+ | `removeEffectsByTag` / `removeEffectsByDefId` | Flag instances for removal next tick |
503
+ | `tickSystems` | Register on `GameWorld` |
504
+
505
+ Lower-level access: `AbilitySystemFacade` is exported for advanced wiring; most games use `createAbilitySystem` only.
506
+
507
+ ### DSL helpers
508
+
509
+ ```typescript
510
+ defineAttribute(def: AttributeDef): AttributeDef
511
+ defineEffect(def: EffectDefInput): EffectDef
512
+ defineAbility(def: AbilityDef): AbilityDef
513
+ defineAbilitySystem(bundle): AbilitySystemDefinitions
514
+ ```
515
+
516
+ ### Events
517
+
518
+ ```typescript
519
+ import {
520
+ ABILITY_ACTIVATED_EVENT,
521
+ type AbilityActivatedEvent,
522
+ Cue,
523
+ CuePresentationSystem,
524
+ GAMEPLAY_CUE_EVENT,
525
+ gameplayCueKey,
526
+ type CueConfig,
527
+ type CueContext,
528
+ type CueFactory,
529
+ type GameplayCueDispatchedEvent,
530
+ } from '@phalanx-engine/abilities';
531
+ ```
532
+
533
+ ### Components and types
534
+
535
+ Exported: `AbilitySystemComponent`, `AbilitiesComponentType`, effect/attribute/tag types, individual systems for custom pipelines.
536
+
537
+ See `src/index.ts` for the full public surface.
538
+
539
+ ## Integration checklist
540
+
541
+ 1. **phalanx-ecs**: `GameWorld`, `Entity`, `resetEntityIdCounter`, register `abilities.tickSystems` in deterministic order alongside movement/physics/combat systems.
542
+ 2. **phalanx-math**: `FP.FromInt`, `FP.FromFloat`, `FP.Add`, `FP.Mul`, etc. for all magnitudes.
543
+ 3. **Client-only cues**: pass a non-empty `cues` map with `Cue` subclasses; presentation runs in `afterFrame`. Never mutate simulation from cue code.
544
+ 4. **User-owned systems**: projectiles, rockets, AoE searches, and Aura ticking stay in game code. Call `applyEffect` or `activateAbility` from these systems on deterministic events (collision, timer tick).
545
+
546
+ ## Testing
547
+
548
+ ```bash
549
+ pnpm --filter @phalanx-engine/abilities test
550
+ ```
551
+
552
+ Tests use `GameWorld.processAllTicks()` with pipeline subsets (`activation`, `effects`, …). See `tests/helpers.ts` for patterns.
553
+
554
+ ## Agent skill
555
+
556
+ For AI-assisted development, use the repository skill:
557
+
558
+ [`skills/phalanx-abilities/SKILL.md`](../skills/phalanx-abilities/SKILL.md)
559
+
560
+ It covers decision trees, the five recipes above, determinism rules, and anti-patterns when extending combat systems.
561
+
562
+ ## License
563
+
564
+ Same as the Phalanx Engine monorepo.
@@ -0,0 +1,34 @@
1
+ import { type EntityManager } from '@phalanx-engine/ecs';
2
+ import type { FixedPoint } from '@phalanx-engine/math';
3
+ import { AttributesComponent } from '../components';
4
+ import type { AbilitySystemRegistries } from '../registry';
5
+ import type { AbilitySystemRuntime, GameplayCueBufferView } from '../runtime';
6
+ import type { AbilityHook, ProvidedTarget } from '../types';
7
+ export interface AttributeValue {
8
+ base: FixedPoint;
9
+ current: FixedPoint;
10
+ }
11
+ export declare const NO_SOURCE_ENTITY_ID = -1;
12
+ export declare class AbilitySystemFacade {
13
+ private readonly entityManager;
14
+ private readonly registries;
15
+ private readonly runtime;
16
+ constructor(entityManager: EntityManager, registries: AbilitySystemRegistries, runtime: AbilitySystemRuntime);
17
+ initAttributesForEntity(entityId: number): AttributesComponent;
18
+ getAttribute(entityId: number, attrId: string): AttributeValue;
19
+ tryGetAttribute(entityId: number, attrId: string): AttributeValue | undefined;
20
+ applyEffect(targetEntityId: number, effectId: string, sourceEntityId?: number): void;
21
+ removeEffectsByTag(entityId: number, grantedTag: string): number;
22
+ removeEffectsByDefId(entityId: number, effectId: string): number;
23
+ activateAbility(casterEntityId: number, abilityId: string, providedTarget?: ProvidedTarget): boolean;
24
+ registerHook(hookId: string, hook: AbilityHook): void;
25
+ hasTag(entityId: number, tag: string): boolean;
26
+ addTag(entityId: number, tag: string): void;
27
+ removeTag(entityId: number, tag: string): boolean;
28
+ get runtimeInternal(): AbilitySystemRuntime;
29
+ get gameplayCueBufferInternal(): GameplayCueBufferView;
30
+ private requireEntity;
31
+ private getOrCreateActiveEffects;
32
+ private getOrCreateTags;
33
+ }
34
+ //# sourceMappingURL=AbilitySystemFacade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AbilitySystemFacade.d.ts","sourceRoot":"","sources":["../../src/api/AbilitySystemFacade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAEL,mBAAmB,EAKpB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;CACrB;AAOD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAWtC,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAFP,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,uBAAuB,EACnC,OAAO,EAAE,oBAAoB;IAOzC,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB;IA4B9D,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc;IAuB9D,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IA2C7E,WAAW,CAChB,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,EAChB,cAAc,GAAE,MAA4B,GAC3C,IAAI;IAmBA,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAgChE,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAwDhE,eAAe,CACpB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO;IA2CH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI;IAQrD,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAuB9C,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAiB3C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAgCxD,IAAW,eAAe,IAAI,oBAAoB,CAEjD;IASD,IAAW,yBAAyB,IAAI,qBAAqB,CAE5D;IAED,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,eAAe;CAUxB"}