@waica/engine 0.17.0 → 0.19.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/dist/game.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import * as THREE from 'three';
2
+ import { AssetLoader } from './assets/asset-loader.js';
3
+ import type { TextureBackend } from './assets/texture-backend.js';
2
4
  import type { AudioBackend } from './audio/backend.js';
3
5
  import { AudioSubsystem } from './audio/audio-subsystem.js';
4
6
  import { type SceneCameraJson } from './camera.js';
7
+ import { CameraEffects } from './camera-effects.js';
5
8
  import type { Component } from './component.js';
6
9
  import { Entity } from './entity.js';
7
10
  import { Emitter } from './events.js';
@@ -38,6 +41,13 @@ export interface GameOptions {
38
41
  * first unlock (CA-6), never eagerly here.
39
42
  */
40
43
  audio?: AudioBackend;
44
+ /**
45
+ * Replaces the real `THREE.TextureLoader` implementation behind
46
+ * `game.assets` (ADR 0013's seam, applied to textures — ADR 0019), mainly
47
+ * for a project's own tests: `happy-dom` decodes no images. Defaults to
48
+ * the real backend either way; `game.assets` always exists.
49
+ */
50
+ textures?: TextureBackend;
41
51
  }
42
52
  export type UpdateFn = (dt: number) => void;
43
53
  export interface SpawnPrefabOptions {
@@ -68,6 +78,17 @@ export declare class Game {
68
78
  readonly ui: GameUi;
69
79
  /** The audio mixer: channels, master, playback. See ADR 0012, ADR 0013. */
70
80
  readonly audio: AudioSubsystem;
81
+ /**
82
+ * The texture cache: keep-all for the Game's life, `preload`, `status`
83
+ * and the Assets Ready promise `ready()`. Session-scoped by construction
84
+ * (ADR 0011): `unloadScene()` never touches it. See ADR 0019.
85
+ */
86
+ readonly assets: AssetLoader;
87
+ /**
88
+ * Shake, Fade and Flash, beside `camera` and never written into it
89
+ * (ADR 0020). A Fade survives `unloadScene()`; a Shake or Flash does not.
90
+ */
91
+ readonly cameraEffects: CameraEffects;
71
92
  /** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
72
93
  readonly time: GameTime;
73
94
  /** Registry retained by loadScene for runtime prefab spawning. */
package/dist/game.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import * as THREE from 'three';
2
2
  import { gameViewport } from './anchored-pieces.js';
3
+ import { AssetLoader } from './assets/asset-loader.js';
3
4
  import { AudioSubsystem } from './audio/audio-subsystem.js';
4
5
  import { dispatchCollisions as dispatchHitboxCollisions } from './collision-dispatch.js';
5
6
  import { isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera, } from './camera.js';
7
+ import { advanceCameraEffects, CameraEffects, layoutCameraEffects } from './camera-effects.js';
6
8
  import { resolveComponentUpdateSchedule } from './component-update-schedule.js';
7
9
  import { Entity } from './entity.js';
8
10
  import { Emitter } from './events.js';
@@ -35,6 +37,17 @@ export class Game {
35
37
  ui;
36
38
  /** The audio mixer: channels, master, playback. See ADR 0012, ADR 0013. */
37
39
  audio;
40
+ /**
41
+ * The texture cache: keep-all for the Game's life, `preload`, `status`
42
+ * and the Assets Ready promise `ready()`. Session-scoped by construction
43
+ * (ADR 0011): `unloadScene()` never touches it. See ADR 0019.
44
+ */
45
+ assets;
46
+ /**
47
+ * Shake, Fade and Flash, beside `camera` and never written into it
48
+ * (ADR 0020). A Fade survives `unloadScene()`; a Shake or Flash does not.
49
+ */
50
+ cameraEffects;
38
51
  /** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
39
52
  time = new GameTime();
40
53
  /** Registry retained by loadScene for runtime prefab spawning. */
@@ -103,6 +116,17 @@ export class Game {
103
116
  // music bed needs across a scene swap.
104
117
  resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
105
118
  });
119
+ this.assets = new AssetLoader({
120
+ backend: options.textures,
121
+ // Same late lookup as audio's: preload() resolves through whatever
122
+ // catalog is registered at call time, which outlives every scene.
123
+ resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
124
+ });
125
+ this.cameraEffects = new CameraEffects({
126
+ host: () => canvas.parentElement ?? document.body,
127
+ // One screen pixel in world units: the shake snaps to it (never the base).
128
+ pixel: () => (this.resolution ? this.viewHeight / this.resolution.height : null),
129
+ });
106
130
  this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
107
131
  this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
108
132
  this.scene.background = new THREE.Color(background);
@@ -160,6 +184,7 @@ export class Game {
160
184
  // Scene-scoped timers/tweens die here too (ADR 0017): before entities are
161
185
  // destroyed below, so an owner's own destroy() cancellation is a no-op.
162
186
  this.time.cancelSceneScoped();
187
+ this.cameraEffects.unloadScene();
163
188
  // An explicit unload means "no scene": a swap queued earlier this frame
164
189
  // would otherwise flush next frame and resurrect one.
165
190
  this.pendingSceneLoad = null;
@@ -310,6 +335,7 @@ export class Game {
310
335
  },
311
336
  loadScene: (name) => this.loadSceneByName(name),
312
337
  availableScenes: () => this.availableScenes,
338
+ assets: () => this.assets.status,
313
339
  });
314
340
  activation.register(this.runtimeBridge);
315
341
  this.audio.setSilenced(true);
@@ -352,11 +378,15 @@ export class Game {
352
378
  this.resizeObserver.disconnect();
353
379
  this.ui.dispose();
354
380
  this.audio.dispose();
381
+ this.cameraEffects.dispose();
355
382
  // Cancels both scopes, including session-scoped work no entity owns
356
383
  // (entity.destroy() below only ever reaches owned work) — ADR 0017.
357
384
  this.time.cancelAll();
358
385
  for (const entity of [...this.entities])
359
386
  entity.destroy();
387
+ // After the entities: their clones go with the cascade above, the
388
+ // cached bases go here, exactly once (ADR 0019).
389
+ this.assets.dispose();
360
390
  this.renderer.dispose();
361
391
  }
362
392
  /**
@@ -461,6 +491,7 @@ export class Game {
461
491
  }
462
492
  this.dispatchCollisions();
463
493
  this.updateSceneCamera(SIMULATION_STEP);
494
+ advanceCameraEffects(this.cameraEffects);
464
495
  this.finishStep();
465
496
  }
466
497
  /** Closes a step (real or the non-simulating stand-in): host callbacks, then the input frame. */
@@ -518,15 +549,28 @@ export class Game {
518
549
  if (this.renderSort === 'y')
519
550
  this.applyYSort();
520
551
  this.ui.setActive(this.simulate);
521
- anchoredPiecesOf(this.ui).place();
522
- if (this.resolution) {
523
- // Letterbox bars: clear the whole canvas, then render inside the scissor.
524
- this.renderer.setScissorTest(false);
525
- this.renderer.setClearColor(0x000000, 1);
526
- this.renderer.clear(true, false, false);
527
- this.renderer.setScissorTest(true);
552
+ // The shake offset exists only while drawing (ADR 0020): Anchored Pieces
553
+ // and the render see the drawn center, then the base comes back exactly,
554
+ // so follow smoothing never starts from its own jitter.
555
+ const { x, y } = this.camera.position;
556
+ const { shake } = this.cameraEffects.state;
557
+ this.camera.position.x = x + shake.x;
558
+ this.camera.position.y = y + shake.y;
559
+ try {
560
+ anchoredPiecesOf(this.ui).place();
561
+ if (this.resolution) {
562
+ // Letterbox bars: clear the whole canvas, then render inside the scissor.
563
+ this.renderer.setScissorTest(false);
564
+ this.renderer.setClearColor(0x000000, 1);
565
+ this.renderer.clear(true, false, false);
566
+ this.renderer.setScissorTest(true);
567
+ }
568
+ this.renderer.render(this.scene, this.camera);
569
+ }
570
+ finally {
571
+ this.camera.position.x = x;
572
+ this.camera.position.y = y;
528
573
  }
529
- this.renderer.render(this.scene, this.camera);
530
574
  }
531
575
  componentUpdateSchedule(entity) {
532
576
  const components = [...entity.components];
@@ -638,5 +682,6 @@ export class Game {
638
682
  this.camera.top = halfH;
639
683
  this.camera.bottom = -halfH;
640
684
  this.camera.updateProjectionMatrix();
685
+ layoutCameraEffects(this.cameraEffects, gameViewport(w, h, this.resolution));
641
686
  }
642
687
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { Game } from './game.js';
2
2
  export type { GameOptions, GameResolution, SceneCatalog, SpawnPrefabOptions, UpdateFn, ParamOverrides, } from './game.js';
3
+ export { AssetLoader } from './assets/asset-loader.js';
4
+ export type { AssetStatus } from './assets/asset-loader.js';
5
+ export type { TextureBackend } from './assets/texture-backend.js';
3
6
  export { AudioSubsystem } from './audio/audio-subsystem.js';
4
7
  export type { AudioSubsystemOptions } from './audio/audio-subsystem.js';
5
8
  export type { AudioBackend, AudioResource, BackendPlayHandle, BackendPlayOptions } from './audio/backend.js';
@@ -14,6 +17,8 @@ export { spritePlacement } from './sprite-placement.js';
14
17
  export type { SpritePlacement, SpritePlacementInput } from './sprite-placement.js';
15
18
  export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
16
19
  export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
20
+ export { CameraEffects } from './camera-effects.js';
21
+ export type { CameraEffectColor, CameraEffectHandle, CameraEffectsOptions, CameraEffectsState, FadeOptions, FlashOptions, ShakeOptions, } from './camera-effects.js';
17
22
  export { GameTime, advanceGameTime } from './game-time.js';
18
23
  export type { EasingName, TimerHandle, TimerOptions, TweenOptions } from './game-time.js';
19
24
  export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
@@ -32,7 +37,7 @@ export type { PointerCamera, PointerDeps, PointerPick, PointerResolution } from
32
37
  export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
33
38
  export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
34
39
  export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
35
- export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
40
+ export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
36
41
  export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
37
42
  export { Stats } from './stats.js';
38
43
  export type { StatValue } from './stats.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Game } from './game.js';
2
+ export { AssetLoader } from './assets/asset-loader.js';
2
3
  export { AudioSubsystem } from './audio/audio-subsystem.js';
3
4
  export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
4
5
  export { isYSortParticipant, ySortZ } from './render-sort.js';
@@ -6,6 +7,7 @@ export { projectIsometric, screenInputToLogical, unprojectIsometric } from './pr
6
7
  export { spritePlacement } from './sprite-placement.js';
7
8
  export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
8
9
  export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
10
+ export { CameraEffects } from './camera-effects.js';
9
11
  export { GameTime, advanceGameTime } from './game-time.js';
10
12
  export { Entity } from './entity.js';
11
13
  export { Component } from './component.js';
@@ -1,3 +1,4 @@
1
+ import type { AssetStatus } from './assets/asset-loader.js';
1
2
  import type { RuntimeSnapshot, RuntimeSnapshotFilters } from './runtime-inspection.js';
2
3
  export declare const RUNTIME_BRIDGE_PROTOCOL_VERSION: 1;
3
4
  export declare const RUNTIME_BRIDGE_SYMBOL: unique symbol;
@@ -10,9 +11,15 @@ export type RuntimeMode = 'paused' | 'real-time';
10
11
  * gating on capabilities check for (review finding #4) — protocol 1 alone
11
12
  * doesn't distinguish an engine that silently no-ops an unknown operation
12
13
  * from one that runs it. `fixed-step` (ADR 0014) announces that `step`
13
- * advances whole 1/60 s Simulation Steps and takes no `dt`.
14
+ * advances whole 1/60 s Simulation Steps and takes no `dt`. `assets`
15
+ * (ADR 0019) announces that every metadata carries `assets: { pending,
16
+ * loaded, failed }` from `game.assets.status`, so a Run Session can wait
17
+ * for Assets Ready (`pending === 0`) at readiness, after a `scene`
18
+ * operation and before a screenshot. `camera-effects` (issue #74, ADR 0020)
19
+ * announces that every snapshot carries `camera: { shake, fade, flash }`
20
+ * from `game.cameraEffects`; the `scene` operation stays a hard cut.
14
21
  */
15
- export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step'];
22
+ export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects'];
16
23
  export interface RuntimeMetadata {
17
24
  bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
18
25
  engineVersion: string;
@@ -20,6 +27,8 @@ export interface RuntimeMetadata {
20
27
  frame: number;
21
28
  simulationTime: number;
22
29
  capabilities: readonly string[];
30
+ /** `game.assets.status` at the moment of the read — reading it never advances `frame`. */
31
+ assets: AssetStatus;
23
32
  }
24
33
  export type RuntimeControlRequest = {
25
34
  operation: 'press' | 'hold' | 'release';
@@ -81,6 +90,8 @@ export interface RuntimeBridgeHost {
81
90
  /** Resolves `name` through the registered catalog and loads it. */
82
91
  loadScene(name: string): boolean;
83
92
  availableScenes(): string[];
93
+ /** A fresh `game.assets.status`. */
94
+ assets(): AssetStatus;
84
95
  }
85
96
  export declare class EngineRuntimeBridge implements RuntimeBridge {
86
97
  readonly surface: HTMLCanvasElement;
@@ -10,9 +10,21 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
10
10
  * gating on capabilities check for (review finding #4) — protocol 1 alone
11
11
  * doesn't distinguish an engine that silently no-ops an unknown operation
12
12
  * from one that runs it. `fixed-step` (ADR 0014) announces that `step`
13
- * advances whole 1/60 s Simulation Steps and takes no `dt`.
13
+ * advances whole 1/60 s Simulation Steps and takes no `dt`. `assets`
14
+ * (ADR 0019) announces that every metadata carries `assets: { pending,
15
+ * loaded, failed }` from `game.assets.status`, so a Run Session can wait
16
+ * for Assets Ready (`pending === 0`) at readiness, after a `scene`
17
+ * operation and before a screenshot. `camera-effects` (issue #74, ADR 0020)
18
+ * announces that every snapshot carries `camera: { shake, fade, flash }`
19
+ * from `game.cameraEffects`; the `scene` operation stays a hard cut.
14
20
  */
15
- export const RUNTIME_BRIDGE_CAPABILITIES = ['click', 'scene', 'fixed-step'];
21
+ export const RUNTIME_BRIDGE_CAPABILITIES = [
22
+ 'click',
23
+ 'scene',
24
+ 'fixed-step',
25
+ 'assets',
26
+ 'camera-effects',
27
+ ];
16
28
  export class RuntimeBridgeOperationError extends Error {
17
29
  code;
18
30
  availableActions;
@@ -61,6 +73,7 @@ export class EngineRuntimeBridge {
61
73
  // Derived, never summed: 60 steps are exactly 1 s, with no float drift.
62
74
  simulationTime: this.frame * SIMULATION_STEP,
63
75
  capabilities: RUNTIME_BRIDGE_CAPABILITIES,
76
+ assets: this.host.assets(),
64
77
  };
65
78
  }
66
79
  inspect(filters = {}) {
@@ -1,4 +1,5 @@
1
1
  import type { AudioChannelState, LiveSoundInfo } from './audio/types.js';
2
+ import type { CameraEffectsState } from './camera-effects.js';
2
3
  import type { Game } from './game.js';
3
4
  import type { RuntimeMetadata } from './runtime-bridge.js';
4
5
  import type { StatValue } from './stats.js';
@@ -106,6 +107,13 @@ export interface RuntimeSnapshotUi {
106
107
  values: Record<string, StatValue | ProjectionMarker> | ProjectionMarker;
107
108
  }>;
108
109
  }
110
+ /**
111
+ * `game.cameraEffects.state` (issue #74 CA-17), beside `time` and `ui`: the
112
+ * shake offset (world units, added to the camera only while drawing) and
113
+ * the Fade and Flash layers' `#rrggbb` color and opacity, all from the
114
+ * last completed Simulation Step. Emitted unconditionally — never filtered.
115
+ */
116
+ export type RuntimeSnapshotCamera = CameraEffectsState;
109
117
  export interface RuntimeSnapshot extends RuntimeMetadata {
110
118
  stats: Record<string, StatValue>;
111
119
  /** The live scene's name (its catalog key), or null with no scene loaded. */
@@ -115,6 +123,7 @@ export interface RuntimeSnapshot extends RuntimeMetadata {
115
123
  audio: RuntimeSnapshotAudio;
116
124
  time: RuntimeSnapshotTime;
117
125
  ui: RuntimeSnapshotUi;
126
+ camera: RuntimeSnapshotCamera;
118
127
  }
119
128
  export declare const RUNTIME_PROJECTION_LIMITS: {
120
129
  readonly depth: 5;
@@ -272,6 +272,7 @@ export class RuntimeInspector {
272
272
  audio: this.audioSnapshot(),
273
273
  time: this.timeSnapshot(),
274
274
  ui: this.uiSnapshot(projectionIssues),
275
+ camera: this.game.cameraEffects.state,
275
276
  });
276
277
  }
277
278
  audioSnapshot() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/engine",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Waica game engine core — archetype-driven, web-first, 2D & 3D",
5
5
  "license": "MIT",
6
6
  "type": "module",