@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.
- package/README.md +564 -0
- package/dist/api/AbilitySystemFacade.d.ts +34 -0
- package/dist/api/AbilitySystemFacade.d.ts.map +1 -0
- package/dist/api/AbilitySystemFacade.js +213 -0
- package/dist/api/createAbilitySystem.d.ts +49 -0
- package/dist/api/createAbilitySystem.d.ts.map +1 -0
- package/dist/api/createAbilitySystem.js +205 -0
- package/dist/api/defineAbility.d.ts +3 -0
- package/dist/api/defineAbility.d.ts.map +1 -0
- package/dist/api/defineAbility.js +9 -0
- package/dist/api/defineAbilitySystem.d.ts +8 -0
- package/dist/api/defineAbilitySystem.d.ts.map +1 -0
- package/dist/api/defineAbilitySystem.js +7 -0
- package/dist/api/defineAttribute.d.ts +3 -0
- package/dist/api/defineAttribute.d.ts.map +1 -0
- package/dist/api/defineAttribute.js +3 -0
- package/dist/api/defineEffect.d.ts +6 -0
- package/dist/api/defineEffect.d.ts.map +1 -0
- package/dist/api/defineEffect.js +23 -0
- package/dist/api/index.d.ts +11 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +6 -0
- package/dist/components/AbilitiesComponentType.d.ts +7 -0
- package/dist/components/AbilitiesComponentType.d.ts.map +1 -0
- package/dist/components/AbilitiesComponentType.js +7 -0
- package/dist/components/AbilitySystemComponent.d.ts +17 -0
- package/dist/components/AbilitySystemComponent.d.ts.map +1 -0
- package/dist/components/AbilitySystemComponent.js +31 -0
- package/dist/components/ActiveEffectsComponent.d.ts +12 -0
- package/dist/components/ActiveEffectsComponent.d.ts.map +1 -0
- package/dist/components/ActiveEffectsComponent.js +6 -0
- package/dist/components/AttributesComponent.d.ts +9 -0
- package/dist/components/AttributesComponent.d.ts.map +1 -0
- package/dist/components/AttributesComponent.js +15 -0
- package/dist/components/GameplayTagsComponent.d.ts +8 -0
- package/dist/components/GameplayTagsComponent.d.ts.map +1 -0
- package/dist/components/GameplayTagsComponent.js +7 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +5 -0
- package/dist/cues/Cue.d.ts +15 -0
- package/dist/cues/Cue.d.ts.map +1 -0
- package/dist/cues/Cue.js +7 -0
- package/dist/cues/index.d.ts +3 -0
- package/dist/cues/index.d.ts.map +1 -0
- package/dist/cues/index.js +1 -0
- package/dist/events/AbilityEvents.d.ts +10 -0
- package/dist/events/AbilityEvents.d.ts.map +1 -0
- package/dist/events/AbilityEvents.js +1 -0
- package/dist/events/CueEvents.d.ts +5 -0
- package/dist/events/CueEvents.d.ts.map +1 -0
- package/dist/events/CueEvents.js +4 -0
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/registry/AbilityHooksRegistry.d.ts +10 -0
- package/dist/registry/AbilityHooksRegistry.d.ts.map +1 -0
- package/dist/registry/AbilityHooksRegistry.js +25 -0
- package/dist/registry/AbilityRegistry.d.ts +6 -0
- package/dist/registry/AbilityRegistry.d.ts.map +1 -0
- package/dist/registry/AbilityRegistry.js +4 -0
- package/dist/registry/AbilitySystemRegistries.d.ts +12 -0
- package/dist/registry/AbilitySystemRegistries.d.ts.map +1 -0
- package/dist/registry/AbilitySystemRegistries.js +12 -0
- package/dist/registry/AttributeRegistry.d.ts +7 -0
- package/dist/registry/AttributeRegistry.d.ts.map +1 -0
- package/dist/registry/AttributeRegistry.js +11 -0
- package/dist/registry/DefinitionRegistry.d.ts +16 -0
- package/dist/registry/DefinitionRegistry.d.ts.map +1 -0
- package/dist/registry/DefinitionRegistry.js +37 -0
- package/dist/registry/EffectRegistry.d.ts +6 -0
- package/dist/registry/EffectRegistry.d.ts.map +1 -0
- package/dist/registry/EffectRegistry.js +4 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +5 -0
- package/dist/runtime/AbilitySystemRuntime.d.ts +26 -0
- package/dist/runtime/AbilitySystemRuntime.d.ts.map +1 -0
- package/dist/runtime/AbilitySystemRuntime.js +11 -0
- package/dist/runtime/GameplayCueBuffer.d.ts +10 -0
- package/dist/runtime/GameplayCueBuffer.d.ts.map +1 -0
- package/dist/runtime/GameplayCueBuffer.js +16 -0
- package/dist/runtime/InstanceIdCounter.d.ts +6 -0
- package/dist/runtime/InstanceIdCounter.d.ts.map +1 -0
- package/dist/runtime/InstanceIdCounter.js +10 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/systems/AbilityActivationSystem.d.ts +23 -0
- package/dist/systems/AbilityActivationSystem.d.ts.map +1 -0
- package/dist/systems/AbilityActivationSystem.js +277 -0
- package/dist/systems/AbilityHookExecutorSystem.d.ts +10 -0
- package/dist/systems/AbilityHookExecutorSystem.d.ts.map +1 -0
- package/dist/systems/AbilityHookExecutorSystem.js +35 -0
- package/dist/systems/AttributeAggregationSystem.d.ts +12 -0
- package/dist/systems/AttributeAggregationSystem.d.ts.map +1 -0
- package/dist/systems/AttributeAggregationSystem.js +117 -0
- package/dist/systems/CueBufferCleanupSystem.d.ts +8 -0
- package/dist/systems/CueBufferCleanupSystem.d.ts.map +1 -0
- package/dist/systems/CueBufferCleanupSystem.js +11 -0
- package/dist/systems/CueDispatchSystem.d.ts +8 -0
- package/dist/systems/CueDispatchSystem.d.ts.map +1 -0
- package/dist/systems/CueDispatchSystem.js +17 -0
- package/dist/systems/CuePresentationSystem.d.ts +14 -0
- package/dist/systems/CuePresentationSystem.d.ts.map +1 -0
- package/dist/systems/CuePresentationSystem.js +50 -0
- package/dist/systems/EffectApplicationSystem.d.ts +20 -0
- package/dist/systems/EffectApplicationSystem.d.ts.map +1 -0
- package/dist/systems/EffectApplicationSystem.js +174 -0
- package/dist/systems/EffectTickSystem.d.ts +16 -0
- package/dist/systems/EffectTickSystem.d.ts.map +1 -0
- package/dist/systems/EffectTickSystem.js +152 -0
- package/dist/systems/index.d.ts +9 -0
- package/dist/systems/index.d.ts.map +1 -0
- package/dist/systems/index.js +8 -0
- package/dist/targeting/TargetResolver.d.ts +19 -0
- package/dist/targeting/TargetResolver.d.ts.map +1 -0
- package/dist/targeting/TargetResolver.js +45 -0
- package/dist/targeting/index.d.ts +3 -0
- package/dist/targeting/index.d.ts.map +1 -0
- package/dist/targeting/index.js +1 -0
- package/dist/types/AbilityDef.d.ts +13 -0
- package/dist/types/AbilityDef.d.ts.map +1 -0
- package/dist/types/AbilityDef.js +1 -0
- package/dist/types/AbilityHook.d.ts +10 -0
- package/dist/types/AbilityHook.d.ts.map +1 -0
- package/dist/types/AbilityHook.js +1 -0
- package/dist/types/ActiveEffectInstance.d.ts +9 -0
- package/dist/types/ActiveEffectInstance.d.ts.map +1 -0
- package/dist/types/ActiveEffectInstance.js +1 -0
- package/dist/types/AttributeDef.d.ts +10 -0
- package/dist/types/AttributeDef.d.ts.map +1 -0
- package/dist/types/AttributeDef.js +1 -0
- package/dist/types/CueEvent.d.ts +9 -0
- package/dist/types/CueEvent.d.ts.map +1 -0
- package/dist/types/CueEvent.js +1 -0
- package/dist/types/EffectDef.d.ts +23 -0
- package/dist/types/EffectDef.d.ts.map +1 -0
- package/dist/types/EffectDef.js +17 -0
- package/dist/types/ModifierOp.d.ts +8 -0
- package/dist/types/ModifierOp.d.ts.map +1 -0
- package/dist/types/ModifierOp.js +1 -0
- package/dist/types/TargetSpec.d.ts +32 -0
- package/dist/types/TargetSpec.d.ts.map +1 -0
- package/dist/types/TargetSpec.js +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- 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"}
|