@waica/engine 0.3.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/dist/aabb.d.ts +2 -0
  3. package/dist/aabb.js +4 -0
  4. package/dist/animation/clip-player.d.ts +20 -0
  5. package/dist/animation/clip-player.js +24 -0
  6. package/dist/animation/contract.d.ts +21 -0
  7. package/dist/animation/contract.js +23 -0
  8. package/dist/animation/sheet.d.ts +65 -0
  9. package/dist/animation/sheet.js +44 -0
  10. package/dist/archetype.d.ts +37 -0
  11. package/dist/archetype.js +1 -0
  12. package/dist/camera.d.ts +71 -0
  13. package/dist/camera.js +59 -0
  14. package/dist/collision-shape.d.ts +26 -0
  15. package/dist/collision-shape.js +136 -0
  16. package/dist/component-registry.d.ts +16 -0
  17. package/dist/component-registry.js +44 -0
  18. package/dist/component.d.ts +58 -0
  19. package/dist/component.js +11 -0
  20. package/dist/components/animated-sprite.d.ts +80 -0
  21. package/dist/components/animated-sprite.js +210 -0
  22. package/dist/components/dynamic-body.d.ts +68 -0
  23. package/dist/components/dynamic-body.js +189 -0
  24. package/dist/components/hitbox.d.ts +25 -0
  25. package/dist/components/hitbox.js +21 -0
  26. package/dist/components/solid.d.ts +32 -0
  27. package/dist/components/solid.js +45 -0
  28. package/dist/components/sprite.d.ts +51 -0
  29. package/dist/components/sprite.js +112 -0
  30. package/dist/entity.d.ts +22 -0
  31. package/dist/entity.js +52 -0
  32. package/dist/events.d.ts +8 -0
  33. package/dist/events.js +20 -0
  34. package/dist/game.d.ts +95 -0
  35. package/dist/game.js +263 -0
  36. package/dist/index.d.ts +39 -0
  37. package/dist/index.js +26 -0
  38. package/dist/input.d.ts +40 -0
  39. package/dist/input.js +84 -0
  40. package/dist/scene.d.ts +80 -0
  41. package/dist/scene.js +93 -0
  42. package/dist/solid-axis.d.ts +21 -0
  43. package/dist/solid-axis.js +77 -0
  44. package/dist/state/hooks.d.ts +90 -0
  45. package/dist/state/hooks.js +81 -0
  46. package/dist/state/state-machine.d.ts +83 -0
  47. package/dist/state/state-machine.js +173 -0
  48. package/dist/stats.d.ts +27 -0
  49. package/dist/stats.js +54 -0
  50. package/dist/ui.d.ts +47 -0
  51. package/dist/ui.js +175 -0
  52. package/package.json +32 -0
@@ -0,0 +1,32 @@
1
+ import { Component } from '../component.js';
2
+ import { type CollisionPoint, type CollisionShape } from '../collision-shape.js';
3
+ /**
4
+ * Static collider. Character motors (e.g. PlatformerMotor) collide against
5
+ * every Solid in the scene.
6
+ *
7
+ * TODO(H1): general dynamic bodies via Rapier; genre character controllers
8
+ * stay hand-rolled so their game feel remains deterministic.
9
+ */
10
+ export declare class Solid extends Component {
11
+ static componentName: string;
12
+ static params: {
13
+ offsetX: {
14
+ label: string;
15
+ };
16
+ offsetY: {
17
+ label: string;
18
+ };
19
+ };
20
+ shape: CollisionShape;
21
+ width: number;
22
+ height: number;
23
+ offsetX: number;
24
+ offsetY: number;
25
+ /** Polygon vertices normalized against width/height. */
26
+ points: CollisionPoint[];
27
+ private get bounds();
28
+ get left(): number;
29
+ get right(): number;
30
+ get top(): number;
31
+ get bottom(): number;
32
+ }
@@ -0,0 +1,45 @@
1
+ import { Component } from '../component.js';
2
+ import { collisionBounds, resolveCollisionPoints, } from '../collision-shape.js';
3
+ /**
4
+ * Static collider. Character motors (e.g. PlatformerMotor) collide against
5
+ * every Solid in the scene.
6
+ *
7
+ * TODO(H1): general dynamic bodies via Rapier; genre character controllers
8
+ * stay hand-rolled so their game feel remains deterministic.
9
+ */
10
+ export class Solid extends Component {
11
+ static componentName = 'Solid';
12
+ static params = {
13
+ offsetX: { label: 'x offset' },
14
+ offsetY: { label: 'y offset' },
15
+ };
16
+ shape = 'rectangle';
17
+ width = 1;
18
+ height = 1;
19
+ offsetX = 0;
20
+ offsetY = 0;
21
+ /** Polygon vertices normalized against width/height. */
22
+ points = resolveCollisionPoints(undefined);
23
+ get bounds() {
24
+ return collisionBounds({
25
+ x: this.entity.position.x + this.offsetX,
26
+ y: this.entity.position.y + this.offsetY,
27
+ width: this.width,
28
+ height: this.height,
29
+ shape: this.shape,
30
+ points: this.points,
31
+ });
32
+ }
33
+ get left() {
34
+ return this.bounds.left;
35
+ }
36
+ get right() {
37
+ return this.bounds.right;
38
+ }
39
+ get top() {
40
+ return this.bounds.top;
41
+ }
42
+ get bottom() {
43
+ return this.bounds.bottom;
44
+ }
45
+ }
@@ -0,0 +1,51 @@
1
+ import { Component } from '../component.js';
2
+ export type SpriteShape = 'rectangle' | 'circle';
3
+ /**
4
+ * Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
5
+ * plane in front of the orthographic camera (see DESIGN.md §6, decision 2).
6
+ */
7
+ export declare class Sprite extends Component {
8
+ static componentName: string;
9
+ static params: {
10
+ offsetX: {
11
+ label: string;
12
+ };
13
+ offsetY: {
14
+ label: string;
15
+ };
16
+ layer: {
17
+ label: string;
18
+ min: number;
19
+ max: number;
20
+ step: number;
21
+ };
22
+ };
23
+ private _width;
24
+ private _height;
25
+ get width(): number;
26
+ set width(value: number);
27
+ get height(): number;
28
+ set height(value: number);
29
+ private _color;
30
+ get color(): number;
31
+ set color(value: number);
32
+ private _offsetX;
33
+ private _offsetY;
34
+ get offsetX(): number;
35
+ set offsetX(value: number);
36
+ get offsetY(): number;
37
+ set offsetY(value: number);
38
+ /** Optional texture URL; with pixelArt on it filters in nearest. */
39
+ texture?: string;
40
+ pixelArt: boolean;
41
+ private _layer;
42
+ get layer(): number;
43
+ set layer(value: number);
44
+ private _shape;
45
+ get shape(): SpriteShape;
46
+ set shape(value: SpriteShape);
47
+ private mesh?;
48
+ onReady(): void;
49
+ onDestroy(): void;
50
+ private createGeometry;
51
+ }
@@ -0,0 +1,112 @@
1
+ import * as THREE from 'three';
2
+ import { Component } from '../component.js';
3
+ const loader = new THREE.TextureLoader();
4
+ /**
5
+ * Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
6
+ * plane in front of the orthographic camera (see DESIGN.md §6, decision 2).
7
+ */
8
+ export class Sprite extends Component {
9
+ static componentName = 'Sprite';
10
+ static params = {
11
+ offsetX: { label: 'x offset' },
12
+ offsetY: { label: 'y offset' },
13
+ layer: { label: 'layer', min: -5, max: 5, step: 1 },
14
+ };
15
+ // Size, color and offset are reactive so inspector edits update the live quad.
16
+ // Texture still needs a rebuild. TODO(H1): fully reactive props.
17
+ _width = 1;
18
+ _height = 1;
19
+ get width() {
20
+ return this._width;
21
+ }
22
+ set width(value) {
23
+ this._width = value;
24
+ this.mesh?.scale.set(this._width, this._height, 1);
25
+ }
26
+ get height() {
27
+ return this._height;
28
+ }
29
+ set height(value) {
30
+ this._height = value;
31
+ this.mesh?.scale.set(this._width, this._height, 1);
32
+ }
33
+ _color = 0xffffff;
34
+ get color() {
35
+ return this._color;
36
+ }
37
+ set color(value) {
38
+ this._color = value;
39
+ this.mesh?.material.color.setHex(value);
40
+ }
41
+ _offsetX = 0;
42
+ _offsetY = 0;
43
+ get offsetX() {
44
+ return this._offsetX;
45
+ }
46
+ set offsetX(value) {
47
+ this._offsetX = value;
48
+ if (this.mesh)
49
+ this.mesh.position.x = value;
50
+ }
51
+ get offsetY() {
52
+ return this._offsetY;
53
+ }
54
+ set offsetY(value) {
55
+ this._offsetY = value;
56
+ if (this.mesh)
57
+ this.mesh.position.y = value;
58
+ }
59
+ /** Optional texture URL; with pixelArt on it filters in nearest. */
60
+ texture;
61
+ pixelArt = false;
62
+ // Draw order among sprites: higher layers render in front. Same-layer
63
+ // sprites fall back to spawn order, so give overlap an explicit layer.
64
+ _layer = 0;
65
+ get layer() {
66
+ return this._layer;
67
+ }
68
+ set layer(value) {
69
+ this._layer = value;
70
+ if (this.mesh)
71
+ this.mesh.position.z = value * 0.01;
72
+ }
73
+ _shape = 'rectangle';
74
+ get shape() {
75
+ return this._shape;
76
+ }
77
+ set shape(value) {
78
+ this._shape = value === 'circle' ? 'circle' : 'rectangle';
79
+ if (!this.mesh)
80
+ return;
81
+ this.mesh.geometry.dispose();
82
+ this.mesh.geometry = this.createGeometry();
83
+ }
84
+ mesh;
85
+ onReady() {
86
+ const material = new THREE.MeshBasicMaterial({ color: this.color, transparent: true });
87
+ if (this.texture) {
88
+ const tex = loader.load(this.texture);
89
+ if (this.pixelArt) {
90
+ tex.magFilter = THREE.NearestFilter;
91
+ tex.minFilter = THREE.NearestFilter;
92
+ }
93
+ tex.colorSpace = THREE.SRGBColorSpace;
94
+ material.map = tex;
95
+ material.color.set(0xffffff);
96
+ }
97
+ this.mesh = new THREE.Mesh(this.createGeometry(), material);
98
+ this.mesh.scale.set(this._width, this._height, 1);
99
+ this.mesh.position.set(this._offsetX, this._offsetY, this.layer * 0.01);
100
+ this.entity.node.add(this.mesh);
101
+ }
102
+ onDestroy() {
103
+ this.mesh?.removeFromParent();
104
+ this.mesh?.geometry.dispose();
105
+ this.mesh?.material.dispose();
106
+ }
107
+ createGeometry() {
108
+ return this._shape === 'circle'
109
+ ? new THREE.CircleGeometry(0.5, 32)
110
+ : new THREE.PlaneGeometry(1, 1);
111
+ }
112
+ }
@@ -0,0 +1,22 @@
1
+ import * as THREE from 'three';
2
+ import type { Component, ComponentClass } from './component.js';
3
+ import type { Game } from './game.js';
4
+ /**
5
+ * A live scene node: a transform (three Group) + components.
6
+ * Created with `game.spawn(name)`.
7
+ */
8
+ export declare class Entity {
9
+ readonly game: Game;
10
+ readonly name: string;
11
+ readonly node: THREE.Group<THREE.Object3DEventMap>;
12
+ readonly components: Component[];
13
+ private destroyed;
14
+ get alive(): boolean;
15
+ constructor(game: Game, name: string);
16
+ get position(): THREE.Vector3;
17
+ get scale(): THREE.Vector3;
18
+ add<T extends Component>(Class: ComponentClass<T>, props?: Partial<T>): T;
19
+ get<T extends Component>(Class: ComponentClass<T>): T | undefined;
20
+ has<T extends Component>(Class: ComponentClass<T>): boolean;
21
+ destroy(): void;
22
+ }
package/dist/entity.js ADDED
@@ -0,0 +1,52 @@
1
+ import * as THREE from 'three';
2
+ /**
3
+ * A live scene node: a transform (three Group) + components.
4
+ * Created with `game.spawn(name)`.
5
+ */
6
+ export class Entity {
7
+ game;
8
+ name;
9
+ node = new THREE.Group();
10
+ components = [];
11
+ destroyed = false;
12
+ get alive() {
13
+ return !this.destroyed;
14
+ }
15
+ constructor(game, name) {
16
+ this.game = game;
17
+ this.name = name;
18
+ }
19
+ get position() {
20
+ return this.node.position;
21
+ }
22
+ get scale() {
23
+ return this.node.scale;
24
+ }
25
+ add(Class, props) {
26
+ const component = new Class();
27
+ component.entity = this;
28
+ component.game = this.game;
29
+ if (props)
30
+ Object.assign(component, props);
31
+ this.game.applyParamOverrides(this, component);
32
+ this.components.push(component);
33
+ component.onReady?.();
34
+ return component;
35
+ }
36
+ get(Class) {
37
+ return this.components.find((c) => c instanceof Class);
38
+ }
39
+ has(Class) {
40
+ return this.components.some((c) => c instanceof Class);
41
+ }
42
+ destroy() {
43
+ if (this.destroyed)
44
+ return;
45
+ this.destroyed = true;
46
+ for (const c of [...this.components])
47
+ c.onDestroy?.();
48
+ this.components.length = 0;
49
+ this.node.removeFromParent();
50
+ this.game.removeEntity(this);
51
+ }
52
+ }
@@ -0,0 +1,8 @@
1
+ type Handler = (...args: unknown[]) => void;
2
+ /** The game's minimal event bus (e.g. 'collect' when picking up a coin). */
3
+ export declare class Emitter {
4
+ private readonly handlers;
5
+ on(event: string, handler: Handler): () => void;
6
+ emit(event: string, ...args: unknown[]): void;
7
+ }
8
+ export {};
package/dist/events.js ADDED
@@ -0,0 +1,20 @@
1
+ /** The game's minimal event bus (e.g. 'collect' when picking up a coin). */
2
+ export class Emitter {
3
+ handlers = new Map();
4
+ on(event, handler) {
5
+ let set = this.handlers.get(event);
6
+ if (!set) {
7
+ set = new Set();
8
+ this.handlers.set(event, set);
9
+ }
10
+ set.add(handler);
11
+ return () => set.delete(handler);
12
+ }
13
+ emit(event, ...args) {
14
+ const set = this.handlers.get(event);
15
+ if (!set)
16
+ return;
17
+ for (const handler of [...set])
18
+ handler(...args);
19
+ }
20
+ }
package/dist/game.d.ts ADDED
@@ -0,0 +1,95 @@
1
+ import * as THREE from 'three';
2
+ import { type SceneCameraJson } from './camera.js';
3
+ import type { Component } from './component.js';
4
+ import { Entity } from './entity.js';
5
+ import { Emitter } from './events.js';
6
+ import { Input, type InputBindings } from './input.js';
7
+ import { type SceneRegistry } from './scene.js';
8
+ import { Stats, type StatValue } from './stats.js';
9
+ import { GameUi } from './ui.js';
10
+ /** Fixed game resolution: the view keeps this aspect, letterboxed. */
11
+ export interface GameResolution {
12
+ width: number;
13
+ height: number;
14
+ }
15
+ export interface GameOptions {
16
+ /** Canvas the game draws into. */
17
+ canvas: HTMLCanvasElement;
18
+ /** Scene background color. */
19
+ background?: THREE.ColorRepresentation;
20
+ /** Visible world height in units; the 2D camera frames this. */
21
+ viewHeight?: number;
22
+ /** Fixed resolution (from the project's game.json); absent = fill the canvas. */
23
+ resolution?: GameResolution;
24
+ /** Control overrides (action → key codes) on top of the defaults. */
25
+ bindings?: InputBindings;
26
+ /** Initial stat values (points, lives…) from the project's stats.json. */
27
+ stats?: Record<string, StatValue>;
28
+ }
29
+ export type UpdateFn = (dt: number) => void;
30
+ export interface SpawnPrefabOptions {
31
+ name?: string;
32
+ position?: [number, number];
33
+ }
34
+ /** Persisted overrides: entity → componentName → prop → value. */
35
+ export type ParamOverrides = Record<string, Record<string, Record<string, number | boolean | string>>>;
36
+ /**
37
+ * Engine core: loop, unified 2D/3D three scene, orthographic camera,
38
+ * entities with components, and input. See DESIGN.md.
39
+ */
40
+ export declare class Game {
41
+ readonly scene: THREE.Scene<THREE.Object3DEventMap>;
42
+ readonly camera: THREE.OrthographicCamera;
43
+ readonly input: Input;
44
+ readonly entities: Entity[];
45
+ readonly events: Emitter;
46
+ readonly stats: Stats;
47
+ /** The HTML UI layer: presentation-only pieces toggled from code. */
48
+ readonly ui: GameUi;
49
+ /** Registry retained by loadScene for runtime prefab spawning. */
50
+ registry: SceneRegistry | null;
51
+ paramOverrides: ParamOverrides;
52
+ /**
53
+ * With false, the loop keeps rendering but runs no component updates
54
+ * or collisions — the editor's edit mode.
55
+ */
56
+ simulate: boolean;
57
+ private readonly renderer;
58
+ private readonly resizeObserver;
59
+ private readonly updateFns;
60
+ private readonly resolution;
61
+ private viewHeight;
62
+ private sceneCamera;
63
+ private lastTime;
64
+ constructor(options: GameOptions);
65
+ /** Creates a live entity in the scene. */
66
+ spawn(name: string): Entity;
67
+ /** Instantiates a registered prefab after a scene has supplied the registry. */
68
+ spawnPrefab(prefab: string, options?: SpawnPrefabOptions): Entity | null;
69
+ /** Finds an entity by name. */
70
+ find(name: string): Entity | undefined;
71
+ /** Loads persisted parameter overrides (waica.params.json). */
72
+ loadParams(url: string): Promise<void>;
73
+ /** Applies persisted overrides to a freshly added component. */
74
+ applyParamOverrides(entity: Entity, component: Component): void;
75
+ /** Registers a function that runs once per frame. Returns the unsubscribe. */
76
+ onUpdate(fn: UpdateFn): () => void;
77
+ /**
78
+ * Adopts a scene's camera block: jumps to its framing and, while
79
+ * simulating, follows/clamps per its settings. Called by loadScene.
80
+ */
81
+ setSceneCamera(json?: SceneCameraJson): void;
82
+ start(): void;
83
+ stop(): void;
84
+ /** Internal: called by Entity.destroy(). */
85
+ removeEntity(entity: Entity): void;
86
+ /** Visible world height (2D camera zoom). */
87
+ get view(): number;
88
+ setViewHeight(value: number): void;
89
+ /** Shuts the game down completely (loop, input, GPU). */
90
+ dispose(): void;
91
+ private tick;
92
+ private updateSceneCamera;
93
+ private dispatchCollisions;
94
+ private resize;
95
+ }