bard-legends-framework 1.5.0 → 1.5.2

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 (2) hide show
  1. package/AGENTS.md +351 -0
  2. package/package.json +3 -2
package/AGENTS.md ADDED
@@ -0,0 +1,351 @@
1
+ # bard-legends-framework — AI Agent Guide
2
+
3
+ > **Audience**: AI coding agents working in games built on `bard-legends-framework`.
4
+ > This file is self-contained. Read it **instead of** the library source in
5
+ > `node_modules/bard-legends-framework`. Everything you need to use the framework correctly is here.
6
+ >
7
+ > **Hard dependency**: the framework is built on `actions-lib` (every observable, stream and
8
+ > lifecycle rule comes from it). You MUST also know that library — read its `AGENTS.md`. In
9
+ > particular: every subscription/stream must be `.attach(owner)`-ed, idle streams self-destruct,
10
+ > pipelines are linear, delivery is synchronous. Those rules are assumed below and not repeated.
11
+
12
+ `bard-legends-framework` is a TypeScript 2D game framework over **PixiJS** (rendering) and **p2**
13
+ (physics). It gives the game a strict, decorator-driven **layered architecture** with dependency
14
+ injection, so gameplay code is organized into self-contained **modules** that talk to each other
15
+ only through narrow public APIs. Lifecycle is `actions-lib` attachment all the way down: every
16
+ entity, view, scene and subscription is owned by a parent and dies when the parent dies.
17
+
18
+ ---
19
+
20
+ ## Core mental model (read this first)
21
+
22
+ 1. **The game is a tree of `actions-lib` attachables.** `Scene` → `Entity` → subscriptions/child
23
+ entities; `Entity` → its `View` → Pixi containers. Destroying any node cascades. You rarely
24
+ call `destroy()` directly — you destroy the owner (close a scene, destroy an entity) and the
25
+ rest follows.
26
+
27
+ 2. **Six roles, each with a decorator.** Every gameplay file is exactly one of these:
28
+
29
+ | Role | Decorator | Extends | Responsibility |
30
+ | --- | --- | --- | --- |
31
+ | **Entity** | `@EntityDecorator()` | `Entity` family | Holds **state** (`Variable`/`Action`/`Reducer`/`Reference`). No rendering, no orchestration. |
32
+ | **View** | `@ViewDecorator({ entity })` | `View` | Renders one entity with Pixi. Auto-created/destroyed with its entity. |
33
+ | **Service** | `@ServiceDecorator()` | (plain class) | Business logic / orchestration. Singleton, injectable. |
34
+ | **Gateway** | `@ServiceDecorator()` | (plain class) | A module's **public API**. The only thing other modules import. |
35
+ | **Controller** | `@ControllerDecorator({ controllerLink })` | (plain class) | Receives gateway calls, forwards to the Service. Hidden from other modules. |
36
+ | **Scene** | `@SceneDecorator()` | `Scene<In, Out>` | A top-level game state (title, battle, map…). Owns its entities; has an update loop. |
37
+
38
+ 3. **Modules are folders; cross-module calls go through Gateways only.** A module
39
+ (`HUD`, `PLAYER_SHIP`, `CAMPAIGN`…) groups one feature's entities/views/services/gateway.
40
+ **Never import another module's Service, Entity or View.** Import its `Gateway` (from the
41
+ module's `⚜️gateways` barrel) and call methods on it. This is what keeps modules decoupled and
42
+ import cycles impossible. (See [Why Gateway + Controller](#why-gateway--controller).)
43
+
44
+ 4. **Dependency injection by constructor type.** Services, Gateways and Controllers receive their
45
+ dependencies as typed constructor parameters — the framework resolves and caches singletons
46
+ (reflect-metadata). Get one outside a constructor with `Service.get(SomeService)`. Do **not**
47
+ `new` a Service/Gateway/Controller yourself.
48
+
49
+ 5. **Two registration mechanisms, both wired at boot by an eager glob.** Controllers (`@Controller`)
50
+ register to their `ControllerLink`; Views (`@View`) register against their entity class. The app
51
+ entry eagerly imports every `*.controller.ts` and `*.view.ts` so those side effects run before
52
+ gameplay starts (see [Bootstrap](#bootstrap)). A new controller/view file does nothing until it
53
+ matches that glob — keep the `.controller.ts` / `.view.ts` suffix.
54
+
55
+ 6. **`actions-lib` is the nervous system.** Entity state is `Variable`/`Action`/`Reducer`. Async
56
+ flows (scene open, animations, `onClose`, gateway results) are `IdleSingleEvent`/`IdleSequence`.
57
+ Attach everything; return idle streams from functions callers may ignore.
58
+
59
+ ---
60
+
61
+ ## Folder convention (one module)
62
+
63
+ Emoji-prefixed folders are load-order / category markers. A typical module:
64
+
65
+ ```
66
+ SOME_MODULE/
67
+ ⚜️gateways/ ← public surface
68
+ some.gateway.ts ← @ServiceDecorator class + `export const XLink = new ControllerLink()`
69
+ controllers/some.controller.ts ← @ControllerDecorator({ controllerLink: XLink })
70
+ dtos/shared.dto.ts ← interfaces crossing the module boundary (DTOs)
71
+ index.ts ← barrel: re-exports the Gateway + DTOs ONLY (never the Service)
72
+ 📐services/some.service.ts ← @ServiceDecorator, real logic (module-private)
73
+ 🧊entities/some.entity.ts ← @EntityDecorator, state (module-private)
74
+ 🧩views/some.view.ts ← @ViewDecorator, rendering (module-private)
75
+ ```
76
+
77
+ Top-level: `🏹scenes/` (Scene classes, may nest their own sub-modules), `⚗️libraries/` (shared UI
78
+ widgets/helpers), `☄️assets/` (generated sprite/definition data). Other modules import from the
79
+ `⚜️gateways/index.ts` barrel — never reach past it.
80
+
81
+ ---
82
+
83
+ ## The roles in detail
84
+
85
+ ### Entity — state only
86
+
87
+ ```ts
88
+ import { Variable, Reducer } from 'actions-lib';
89
+ import { EntityDecorator, SingletonEntity } from 'bard-legends-framework';
90
+
91
+ @EntityDecorator()
92
+ export class HudEntity extends SingletonEntity {
93
+ readonly barState = new Variable<StatusBarState>(DEFAULT);
94
+ readonly isTransitioning = Reducer.createOr();
95
+ constructor(public readonly pilotThumbnail: SpriteID) { super(); }
96
+ }
97
+ ```
98
+
99
+ - An Entity is an `IDAttachable`: it has `.id`, `onDestroy()`, and must be attached to a scope
100
+ (`new HudEntity(...).attach(Scene.getActiveSceneOrFail())`). When the scope dies, the entity and
101
+ all state subscriptions die.
102
+ - **Put only state here.** No Pixi, no orchestration, no cross-module calls. Logic lives in the
103
+ Service; rendering in the View.
104
+ - Static lookups: `MyEntity.getInstanceByID(id)`, `getInstanceByIDOrFail(id)`, `getEntities()`.
105
+
106
+ **Entity base classes** — pick the narrowest that fits:
107
+
108
+ | Base | Use for |
109
+ | --- | --- |
110
+ | `Entity` | Plain stateful object, many instances. |
111
+ | `SingletonEntity` | At most one alive at a time. Adds `getInstance()/getInstanceOrFail()`, `getInstanceAsync(): IdleSingleEvent`, `continuesSubscription()` (open/close stream). |
112
+ | `MovableEntity` | Has `position`/`rotation`/`velocity`/`rotationalSpeed` as `Variable`s, no physics body. |
113
+ | `MovablePhysicsEntity` | A **dynamic** p2 body; the framework drives its `position`/`rotation`/`velocity` Variables from the simulation. Has `onCollision`, `mass`, `area`. |
114
+ | `ImmovablePhysicsEntity` | A **static** p2 body (walls, platforms). |
115
+
116
+ ### View — render one entity
117
+
118
+ ```ts
119
+ import { Container, Game, Service, Sprite, View, ViewDecorator } from 'bard-legends-framework';
120
+
121
+ @ViewDecorator({ entity: HudEntity })
122
+ export class HUDView extends View {
123
+ private hudService = Service.get(HudService);
124
+ constructor(private entity: HudEntity, private someGateway: SomeGateway) { // DI: entity first, then injectables
125
+ super();
126
+ let container = new Container().displayParent(Game.camera.layers.foregroundScreen).attach(this);
127
+ this.entity.barState.subscribe(v => bar.setState(v)).attach(this); // attach to the view
128
+ }
129
+ }
130
+ ```
131
+
132
+ - **Auto-lifecycle**: when an entity of the decorated class is created, its View is instantiated
133
+ automatically (constructor args = the entity, then DI-resolved services/gateways). When the
134
+ entity is destroyed, the View is destroyed. **You never `new` or attach a View yourself.**
135
+ - The View is the owner for all its Pixi objects and subscriptions: end every chain with
136
+ `.attach(this)`.
137
+ - A View **reads** entity state and reflects it; it should not mutate game state. User input →
138
+ call a `Service` method (often `Service.get(...)`), which mutates the entity.
139
+ - `View.update(time, delta)` exists if you need per-frame work; static `View.getInstance(entityID)`.
140
+
141
+ ### Service — logic & orchestration
142
+
143
+ ```ts
144
+ @ServiceDecorator()
145
+ export class HudService {
146
+ constructor(private playerShipGateway: PlayerShipGateway, private optionsMenuGateway: OptionsMenuGateway) {}
147
+ createUI(options: CreateHudOptionsDTO): void {
148
+ let hud = new HudEntity(/*…*/).attach(Scene.getActiveSceneOrFail());
149
+ this.playerShipGateway.subscribeToShipResourceStates()
150
+ .tap(s => { hud.barState.value = toBarState(s); })
151
+ .attach(hud);
152
+ }
153
+ }
154
+ ```
155
+
156
+ - Singleton, constructor-injected. The only role allowed to **create entities** and run async
157
+ orchestration. Owns subscriptions by attaching them to a relevant entity/scene.
158
+ - It may depend on other modules — **but only via their Gateways** (injected). It may use its own
159
+ module's Service/Entity/View directly.
160
+
161
+ ### Why Gateway + Controller
162
+
163
+ These two together let module B call module A without B importing A's internals (no cycles, no
164
+ leaked private classes):
165
+
166
+ ```ts
167
+ // A/⚜️gateways/a.gateway.ts — imported by other modules
168
+ export const ALink = new ControllerLink();
169
+ @ServiceDecorator()
170
+ export class AGateway {
171
+ doThing(x: number): void { return ALink.trigger('doThing', x); } // public, thin, type-safe
172
+ }
173
+
174
+ // A/⚜️gateways/controllers/a.controller.ts — module-private, registered by eager glob
175
+ @ControllerDecorator({ controllerLink: ALink })
176
+ export class AController {
177
+ constructor(private aService: AService) {}
178
+ doThing(x: number): void { this.aService.doThing(x); } // forwards to real logic
179
+ }
180
+ ```
181
+
182
+ - The **Gateway** is a tiny, dependency-free facade other modules import. It holds no logic — each
183
+ method just `link.trigger('methodName', ...args)` and returns the result.
184
+ - The **Controller** binds to the same `ControllerLink` and dispatches the trigger to the real
185
+ `Service`. It lives behind the module boundary and is loaded via the eager `*.controller.ts` glob.
186
+ - Net effect: importing `AGateway` pulls in **none** of A's services/entities. The link is resolved
187
+ at runtime, breaking the static import graph. Gateway method signatures and DTOs (in `dtos/`) are
188
+ the module's contract.
189
+ - **Rule**: keep Gateways logic-free passthroughs; keep their method/DTO names stable; never expose
190
+ an Entity/Service instance across the boundary — cross with plain DTOs.
191
+
192
+ ### Scene — a top-level game state with an update loop
193
+
194
+ ```ts
195
+ @SceneDecorator()
196
+ export class TitleScene extends Scene<void, TitleSceneOutputDTO> { // <Input, Output>
197
+ constructor(private cameraGateway: CameraGateway, private cockpitGateway: CockpitGateway) { super(); }
198
+ protected init(): IdleSingleEvent { // build the scene; return when ready
199
+ this.cockpitGateway.create().tap(out => this.close(out)).attach(this);
200
+ return Game.camera.appear(true, 'pureBlack');
201
+ }
202
+ protected update(time: number, delta: number): void { /* per-frame */ }
203
+ protected prepareToClose(): IdleSingleEvent { return Game.camera.appear(false, 'pureBlack'); }
204
+ }
205
+ ```
206
+
207
+ - `Scene<InputType, OutputType>`: typed input passed to `open`, typed result delivered via `close`.
208
+ - **Lifecycle hooks (abstract, you implement):** `init(input)` build & return readiness;
209
+ `update(time, delta)` called every frame while active; `prepareToClose()` run an exit transition.
210
+ - **Open / close (callers):**
211
+ ```ts
212
+ BattleScene.open({ battle }) // static; IdleSingleEvent<BattleScene>
213
+ .asyncMap(scene => scene.onClose) // resolves with OutputType when the scene closes itself
214
+ .tap(result => /* route to next scene */)
215
+ .attach(owner);
216
+ ```
217
+ Inside the scene, `this.close(output)` triggers `prepareToClose()` then resolves `onClose`.
218
+ - Only one scene is active at a time. Statics: `Scene.activeScene`, `Scene.getActiveSceneOrFail()`,
219
+ `Scene.isTransitioning`, `Scene.cancelActiveScene()`, `MyScene.getInstanceOrFail()`,
220
+ `MyScene.isActive()`.
221
+ - A scene is the natural **owner** for the entities it spawns: `new XEntity().attach(scene)`.
222
+ - `Scene.destroy()` is `@deprecated` — drive lifecycle with `open`/`close`/`cancelActiveScene`.
223
+
224
+ ---
225
+
226
+ ## Bootstrap
227
+
228
+ ```ts
229
+ import { Game, Service } from 'bard-legends-framework';
230
+
231
+ // MANDATORY: register every controller and view via side-effecting import (Vite glob).
232
+ import.meta.glob('./**/*.controller.ts', { eager: true });
233
+ import.meta.glob('./**/*.view.ts', { eager: true });
234
+
235
+ let game = new Game({ devMode, resolution: 2, maxScreenResolution });
236
+ await game.setup({ assetDefinitions: [...SpriteAssets, ...FontAssets], spriteDefinitions: SpriteDefinitions });
237
+ Service.get(GameCycleGateway).startGame(); // kick off the first scene via a gateway
238
+ ```
239
+
240
+ - `Game` is the global singleton: `Game.instance`, `Game.camera`, `Game.time`, `Game.pause`
241
+ (a `Reducer<boolean>` — open an effect to pause), `Game.instance.screenSize`/`screenSizeCenter`
242
+ (`PersistentNotifier<Vector>`), `setResolution`, `screenPositonToStagePosition`.
243
+ - `setup` loads assets and must be awaited before any scene opens.
244
+ - If you add a module, no manual registration is needed **as long as** files keep the
245
+ `*.controller.ts` / `*.view.ts` suffixes so the globs pick them up.
246
+
247
+ ---
248
+
249
+ ## Rendering (Pixi wrappers)
250
+
251
+ All display objects extend `Container` (itself `ContainerAttributes`). They are chainable and are
252
+ `actions-lib` attachables — **attach every one you create**.
253
+
254
+ ```ts
255
+ new Sprite(def) // or Sprite.createByName('spriteId')
256
+ .setPosition(pos, { holdFrom: Vector.half }) // anchor-aware placement; round defaults true
257
+ .setScale(0.75)
258
+ .setZIndex(10)
259
+ .displayParent(parent) // parent: a Container OR a Game.camera.layers.* id
260
+ .attach(owner); // lifecycle owner (usually `this` in a View)
261
+ ```
262
+
263
+ - **Containers**: `Container`, `Sprite`, `Graphics` (factory shapes: `createRectangle`,
264
+ `createCircle`, `createPolygon`, `createArrow`, `createDashedLine`…), `Text`, `RichText`,
265
+ `Placeholder`, plus layout helpers `DisplayObjectArray<T>` (diffs a list via `trackBy`),
266
+ `ScrollAreaUI`, menus (`MenuUI`/`MenuEntity`/`MenuView`).
267
+ - **Attributes** (on every container): `position/x/y`, `size`, `rotation`/`rotationValue`,
268
+ `zIndex`, `scale`, `alpha`, `skewX/Y`, `mirror`, `flip`, `aspectRatio`, `interactive`, `cursor`,
269
+ `setMask`, `boundingBox`, `hitTest`. Setters come in fluent `setX()` and property forms.
270
+ - **Display tree vs. ownership are separate**: `displayParent()` controls where it renders (and
271
+ camera layer); `attach()` controls when it's destroyed. Set both.
272
+ - **Camera layers** (`Game.camera.layers[...]`): `backgroundScreen`, `background`, `main`,
273
+ `foreground`, `foregroundScreen`. `*Screen` layers are fixed to the screen (HUD/menus); the
274
+ others move with the camera (world). `Game.camera` also has `setPosition`, `setTransition`,
275
+ `appear(on, type)`.
276
+ - **Input**: `container.on(ContainerEventType.Click)` returns a `Sequence` (attach it); UI widgets
277
+ expose `onClick`. Global input via injected `KeyboardService` / `MouseService`.
278
+
279
+ ---
280
+
281
+ ## Animation
282
+
283
+ - `Animator(target, 'propName' | ['a','b'], options)` — tween numeric properties; `.animate({...})`
284
+ returns a `SingleEvent<void>`, plus `.set(...)`, `.completeAnimation()`, `.value` notifier.
285
+ - `Animations.{lineer,easeIn,easeOut,easeInOut,easeInOutCubic,blink}` and
286
+ `AnimationInterpolationFunctions` for easing curves.
287
+ - State-driven helpers: `StateAnimation<T>`, `FadeInStateAnimation<T>`, `SlideStateAnimation`,
288
+ `FadeInContent<T>`, `SlideInContent<T>` — drive enter/leave transitions by `setState`/`setIndex`.
289
+ - `UpdateCycle` is the frame clock: `sceneUpdateAction` (+`before`/`after`), `wait(duration)`,
290
+ `lastDelta`, `registerUpdateModifier` (e.g. time-scale). Prefer the scene's `update()` hook over
291
+ subscribing directly unless you need global timing.
292
+
293
+ ---
294
+
295
+ ## Physics (p2)
296
+
297
+ - Bodies are entities: extend `MovablePhysicsEntity` (dynamic) or `ImmovablePhysicsEntity` (static).
298
+ Construct with a `physicsWorldID` and a `PhysicsEntityDefinition` (`shapeDefinition` =
299
+ material/group/shapeType/shapeData, plus `position`, `rotation`, `addInEmptySpace`,
300
+ `includeOnPathfinding`). The framework keeps the entity's `position`/`rotation`/`velocity`
301
+ Variables in sync with the simulation.
302
+ - Collisions: subscribe to `entity.onCollision` (`CollisionReport[]` with `self`/`target` details:
303
+ normal, mass, area, contact position).
304
+ - World & queries via injected `PhysicsGateway`: `createPhysicsWorld`, `hitTest`, `findPath` /
305
+ `findPathDirection`, `applyImpulse`, `createExplosion` / `createElipticExplosion` (return
306
+ `ExplosionHit[]`), `getMapSize`, `setPaused`.
307
+ - Bodies belong to `PhysicsBodyGroup`s; which groups interact is set per-world via
308
+ `interactingBodyGroups`. Pathfinding helpers: `PathFinder`, `VectorFieldPathFinder`,
309
+ `ClosestAvailableSpaceHelper`.
310
+
311
+ ---
312
+
313
+ ## Services you inject (built-ins)
314
+
315
+ Resolve via constructor injection or `Service.get(X)`:
316
+
317
+ - `KeyboardService` — `keyPressed`/`keyReleased` notifiers, `isKeyDown(code, { onlyPressedThisFrame })`.
318
+ - `MouseService` — `stagePosition`/`screenPosition` (world vs. screen), `mainButtonState`,
319
+ `secondaryButtonState`, `initialMouseMovementHappened`.
320
+ - `MouseTargetFocusService` — “did the user click-to-focus a world point” events.
321
+ - `CameraGateway` / `PhysicsGateway` — camera & physics façades shown above.
322
+
323
+ ---
324
+
325
+ ## Testing
326
+
327
+ - Call `BardLegendsHardReset.hardReset()` in `beforeEach` (alongside `actions-lib`'s
328
+ `ActionLib.hardReset()`) to clear entity/singleton/DI registries between tests.
329
+ - Drive frames with `UpdateCycle.triggerUpdateTick(delta)`; use fake timers for `wait`/animations.
330
+ - Because delivery is synchronous (`actions-lib`), most assertions need no awaiting.
331
+
332
+ ---
333
+
334
+ ## Do / Don't
335
+
336
+ ```ts
337
+ // DO: keep state in the Entity, logic in the Service, pixels in the View.
338
+ // DO: cross module boundaries only through an injected Gateway + DTOs.
339
+ // DO: attach every container, subscription and stream to a clear owner (usually `this`/the entity/the scene).
340
+ // DO: create entities in Services; let Views appear automatically; own entities with the Scene.
341
+ // DO: return IdleSingleEvent/IdleSequence from async functions; route scenes via open().asyncMap(s => s.onClose).
342
+ // DO: keep Gateways as thin link.trigger(...) passthroughs; keep DTO/method names stable.
343
+
344
+ // DON'T: import another module's Service/Entity/View — import its Gateway.
345
+ // DON'T: new or attach a View yourself, or new a Service/Gateway/Controller — use DI / Service.get / let entities spawn views.
346
+ // DON'T: put logic in a Gateway, or Pixi/orchestration in an Entity.
347
+ // DON'T: rename a *.controller.ts / *.view.ts file off-suffix — it silently stops registering.
348
+ // DON'T: call scene.destroy() (deprecated) — use close()/cancelActiveScene().
349
+ // DON'T: mutate game state from a View — call a Service method instead.
350
+ // DON'T: forget actions-lib rules (attach-or-throw, linear pipelines, no raw Promises) — see its AGENTS.md.
351
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bard-legends-framework",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Bard Legends Framework",
5
5
  "main": "dist/index.js",
6
6
  "publishConfig": {
@@ -15,7 +15,8 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "AGENTS.md"
19
20
  ],
20
21
  "repository": {
21
22
  "type": "git",