@waica/engine 0.4.2 → 0.6.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 +85 -0
- package/dist/authoring-defaults.d.ts +11 -0
- package/dist/authoring-defaults.js +51 -0
- package/dist/component-update-schedule.d.ts +39 -0
- package/dist/component-update-schedule.js +157 -0
- package/dist/component.d.ts +15 -1
- package/dist/component.js +2 -0
- package/dist/components/animated-sprite.d.ts +2 -0
- package/dist/components/animated-sprite.js +11 -0
- package/dist/components/sprite.d.ts +1 -0
- package/dist/components/sprite.js +1 -0
- package/dist/game.d.ts +7 -0
- package/dist/game.js +79 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +4 -0
- package/dist/input.d.ts +10 -0
- package/dist/input.js +40 -2
- package/dist/runtime-bridge.d.ts +69 -0
- package/dist/runtime-bridge.js +105 -0
- package/dist/runtime-inspection.d.ts +71 -0
- package/dist/runtime-inspection.js +279 -0
- package/dist/state/state-machine.d.ts +2 -1
- package/dist/state/state-machine.js +7 -1
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# `@waica/engine`
|
|
2
|
+
|
|
3
|
+
Waica's public engine core: entities and components, the game loop, scene and prefab loading, state machines, input, collisions, sprites, camera, stats, and UI.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Component, Game, loadScene } from '@waica/engine'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Component lifecycle
|
|
10
|
+
|
|
11
|
+
Waica keeps the lifecycle boundaries distinct:
|
|
12
|
+
|
|
13
|
+
1. `Entity.add()` mounts a component and calls its `onReady` immediately. This remains component insertion order so setup behavior does not silently move.
|
|
14
|
+
2. During each simulated frame, entities keep their existing entity order. When an entity's turn begins, `Game` snapshots and resolves that entity's component `onUpdate` schedule, then dispatches only that schedule.
|
|
15
|
+
3. Physical `onContact` hooks run from `DynamicBody` while it updates. Hitbox `onCollide` hooks run after all entity component updates. Their existing component dispatch order is unchanged.
|
|
16
|
+
4. `Game.onUpdate` callbacks run after component updates, collisions, and camera work; input end-of-frame handling follows them.
|
|
17
|
+
5. `Entity.destroy()` calls `onDestroy` in component insertion order.
|
|
18
|
+
|
|
19
|
+
Only classes whose prototype chain implements `onUpdate` participate in the update schedule. Passive components remain available to their siblings but receive no update position.
|
|
20
|
+
|
|
21
|
+
## Declaring update constraints
|
|
22
|
+
|
|
23
|
+
An updateable component can declare the sibling writes it must observe with inherited static `updateAfter` metadata:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { Component, StateMachine } from '@waica/engine'
|
|
27
|
+
|
|
28
|
+
export class DamageFlash extends Component {
|
|
29
|
+
static override componentName = 'DamageFlash'
|
|
30
|
+
static override updateAfter: readonly string[] = ['StateMachine']
|
|
31
|
+
|
|
32
|
+
override onUpdate(dt: number): void {
|
|
33
|
+
const state = this.entity.get(StateMachine)?.current
|
|
34
|
+
// This update observes StateMachine's state for the same frame.
|
|
35
|
+
void state
|
|
36
|
+
void dt
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The relation is conditional on co-presence. `DamageFlash` does not require a `StateMachine`; when that target is registered but absent from this entity, no edge and no issue are created. A subclass inherits `updateAfter` when it declares nothing and replaces the inherited list when it declares its own list.
|
|
42
|
+
|
|
43
|
+
Constraints always win over the tie-break. Whenever several components are ready simultaneously, Waica compares their case-sensitive `componentName` values in ascending Unicode code-unit order. It never uses locale collation, prefab order, scene order, or editor card grouping. Repeated names in one `updateAfter` list describe one edge.
|
|
44
|
+
|
|
45
|
+
## Invalid schedules fail closed
|
|
46
|
+
|
|
47
|
+
Component identity must be unique within an entity, and both sides of a present constraint must implement `onUpdate`. Waica rejects duplicate component names, unknown targets, passive declarers or present passive targets, self-edges, and multi-component cycles.
|
|
48
|
+
|
|
49
|
+
At runtime an invalid entity runs no partial update schedule and does not fall back to authored order. Other entities continue updating. The engine logs one diagnostic containing the entity and causes, then logs again only if that entity's composition changes.
|
|
50
|
+
|
|
51
|
+
Tools can inspect a composition without constructing components:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { resolveComponentUpdateSchedule, type ComponentClass } from '@waica/engine'
|
|
55
|
+
|
|
56
|
+
const registry: Record<string, ComponentClass> = { DamageFlash, StateMachine }
|
|
57
|
+
const result = resolveComponentUpdateSchedule(
|
|
58
|
+
['DamageFlash', 'StateMachine'],
|
|
59
|
+
registry,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if (result.ok) {
|
|
63
|
+
console.log(result.order) // ['StateMachine', 'DamageFlash']
|
|
64
|
+
} else {
|
|
65
|
+
console.error(result.issues)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The resolver is pure. Pass the effective component-name list and the complete class registry, including project-owned classes. A valid result contains `order` and no issues; an invalid result contains typed, actionable issues and no executable order.
|
|
70
|
+
|
|
71
|
+
## Runtime inspection
|
|
72
|
+
|
|
73
|
+
The engine owns Runtime Bridge protocol 1, but it is dormant during ordinary execution: there is no string-named global, network endpoint or per-frame bridge work. An MCP-owned browser context can install the symbol-keyed ephemeral activation hook before navigation. In that context `Game.start()` registers the fully constructed Game at a paused frame-zero baseline; `Game.dispose()` or page unload unregisters it.
|
|
74
|
+
|
|
75
|
+
Runtime Snapshots automatically inspect public own component fields and setter-backed accessors while excluding `_` fields, `entity`, `game` and functions. A component can replace automatic discovery with the optional public contract:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
class PathFinder extends Component {
|
|
79
|
+
inspectState(): unknown {
|
|
80
|
+
return { target: this.target, remaining: this.path.length }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The return value still passes through the bounded safe projector; it is not serialized with arbitrary `toJSON()`. The package root exports the Runtime Snapshot, projection marker, metadata, control and activation types plus `RUNTIME_BRIDGE_PROTOCOL_VERSION` and `RUNTIME_PROJECTION_LIMITS`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentClass } from './component.js';
|
|
2
|
+
/**
|
|
3
|
+
* The public authoring surface of a component class: own enumerable fields
|
|
4
|
+
* plus setter-backed accessors (walking the whole prototype chain, so a
|
|
5
|
+
* getter/setter pair inherited from a base class counts), read through the
|
|
6
|
+
* getter so the value is the public one. Excludes `_`-prefixed keys, the
|
|
7
|
+
* base Component wiring (`entity`, `game`), anything the class (or an
|
|
8
|
+
* ancestor) declares `transient`, and values that are `undefined` or not
|
|
9
|
+
* JSON-serializable (`Map`, `Set`, functions, class instances).
|
|
10
|
+
*/
|
|
11
|
+
export declare function authoringDefaults(Class: ComponentClass, onError?: (error: unknown) => void): Record<string, unknown>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const EXCLUDED_KEYS = new Set(['entity', 'game']);
|
|
2
|
+
function isSerializable(value) {
|
|
3
|
+
if (value === null)
|
|
4
|
+
return true;
|
|
5
|
+
const kind = typeof value;
|
|
6
|
+
if (kind === 'string' || kind === 'number' || kind === 'boolean')
|
|
7
|
+
return true;
|
|
8
|
+
if (kind !== 'object')
|
|
9
|
+
return false;
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return true;
|
|
12
|
+
const proto = Object.getPrototypeOf(value);
|
|
13
|
+
return proto === Object.prototype || proto === null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The public authoring surface of a component class: own enumerable fields
|
|
17
|
+
* plus setter-backed accessors (walking the whole prototype chain, so a
|
|
18
|
+
* getter/setter pair inherited from a base class counts), read through the
|
|
19
|
+
* getter so the value is the public one. Excludes `_`-prefixed keys, the
|
|
20
|
+
* base Component wiring (`entity`, `game`), anything the class (or an
|
|
21
|
+
* ancestor) declares `transient`, and values that are `undefined` or not
|
|
22
|
+
* JSON-serializable (`Map`, `Set`, functions, class instances).
|
|
23
|
+
*/
|
|
24
|
+
export function authoringDefaults(Class, onError) {
|
|
25
|
+
let instance;
|
|
26
|
+
try {
|
|
27
|
+
instance = new Class();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
onError?.(error);
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const transient = new Set(Class.transient ?? []);
|
|
34
|
+
const keys = new Set(Object.keys(instance));
|
|
35
|
+
for (let proto = Object.getPrototypeOf(instance); proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
|
|
36
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(proto))) {
|
|
37
|
+
if (descriptor.set)
|
|
38
|
+
keys.add(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const result = {};
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
if (EXCLUDED_KEYS.has(key) || key.startsWith('_') || transient.has(key))
|
|
44
|
+
continue;
|
|
45
|
+
const value = instance[key];
|
|
46
|
+
if (value === undefined || !isSerializable(value))
|
|
47
|
+
continue;
|
|
48
|
+
result[key] = value;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComponentClass } from './component.js';
|
|
2
|
+
export type InvalidUpdateConstraintReason = 'unknown-target' | 'passive-declarer' | 'passive-target' | 'self-edge' | 'unregistered-component';
|
|
3
|
+
export interface DuplicateComponentUpdateIssue {
|
|
4
|
+
readonly code: 'duplicate-component';
|
|
5
|
+
readonly componentNames: readonly string[];
|
|
6
|
+
readonly componentName: string;
|
|
7
|
+
readonly count: number;
|
|
8
|
+
readonly cause: string;
|
|
9
|
+
}
|
|
10
|
+
export interface InvalidComponentUpdateConstraintIssue {
|
|
11
|
+
readonly code: 'invalid-update-constraint';
|
|
12
|
+
readonly componentNames: readonly string[];
|
|
13
|
+
readonly declarer: string;
|
|
14
|
+
readonly target?: string;
|
|
15
|
+
readonly reason: InvalidUpdateConstraintReason;
|
|
16
|
+
readonly cause: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ComponentUpdateCycleIssue {
|
|
19
|
+
readonly code: 'component-update-cycle';
|
|
20
|
+
readonly componentNames: readonly string[];
|
|
21
|
+
readonly cause: string;
|
|
22
|
+
}
|
|
23
|
+
export type ComponentUpdateScheduleIssue = DuplicateComponentUpdateIssue | InvalidComponentUpdateConstraintIssue | ComponentUpdateCycleIssue;
|
|
24
|
+
export interface ValidComponentUpdateSchedule {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly order: readonly string[];
|
|
27
|
+
readonly issues: readonly [];
|
|
28
|
+
}
|
|
29
|
+
export interface InvalidComponentUpdateSchedule {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly issues: readonly ComponentUpdateScheduleIssue[];
|
|
32
|
+
}
|
|
33
|
+
export type ComponentUpdateScheduleResult = ValidComponentUpdateSchedule | InvalidComponentUpdateSchedule;
|
|
34
|
+
export type ComponentUpdateRegistry = Readonly<Record<string, ComponentClass | undefined>>;
|
|
35
|
+
/**
|
|
36
|
+
* Resolves one entity's deterministic component update schedule without
|
|
37
|
+
* constructing components or mutating either input.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveComponentUpdateSchedule(componentNames: readonly string[], registry: ComponentUpdateRegistry): ComponentUpdateScheduleResult;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
function codeUnitCompare(left, right) {
|
|
2
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
3
|
+
}
|
|
4
|
+
function registeredClass(registry, componentName) {
|
|
5
|
+
return Object.hasOwn(registry, componentName) ? registry[componentName] : undefined;
|
|
6
|
+
}
|
|
7
|
+
function updates(Class) {
|
|
8
|
+
return typeof Class?.prototype.onUpdate === 'function';
|
|
9
|
+
}
|
|
10
|
+
function updateCycles(nodes, outgoing) {
|
|
11
|
+
let nextIndex = 0;
|
|
12
|
+
const indices = new Map();
|
|
13
|
+
const lowLinks = new Map();
|
|
14
|
+
const stack = [];
|
|
15
|
+
const onStack = new Set();
|
|
16
|
+
const cycles = [];
|
|
17
|
+
const visit = (node) => {
|
|
18
|
+
const index = nextIndex++;
|
|
19
|
+
indices.set(node, index);
|
|
20
|
+
lowLinks.set(node, index);
|
|
21
|
+
stack.push(node);
|
|
22
|
+
onStack.add(node);
|
|
23
|
+
for (const dependent of [...(outgoing.get(node) ?? [])].sort(codeUnitCompare)) {
|
|
24
|
+
if (!indices.has(dependent)) {
|
|
25
|
+
visit(dependent);
|
|
26
|
+
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(dependent)));
|
|
27
|
+
}
|
|
28
|
+
else if (onStack.has(dependent)) {
|
|
29
|
+
lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(dependent)));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (lowLinks.get(node) !== indices.get(node))
|
|
33
|
+
return;
|
|
34
|
+
const group = [];
|
|
35
|
+
while (stack.length > 0) {
|
|
36
|
+
const member = stack.pop();
|
|
37
|
+
onStack.delete(member);
|
|
38
|
+
group.push(member);
|
|
39
|
+
if (member === node)
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (group.length > 1)
|
|
43
|
+
cycles.push(group.sort(codeUnitCompare));
|
|
44
|
+
};
|
|
45
|
+
for (const node of nodes) {
|
|
46
|
+
if (!indices.has(node))
|
|
47
|
+
visit(node);
|
|
48
|
+
}
|
|
49
|
+
return cycles.sort((left, right) => codeUnitCompare(left[0], right[0]));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolves one entity's deterministic component update schedule without
|
|
53
|
+
* constructing components or mutating either input.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveComponentUpdateSchedule(componentNames, registry) {
|
|
56
|
+
const present = new Set(componentNames);
|
|
57
|
+
const nodes = [...present]
|
|
58
|
+
.filter((name) => updates(registeredClass(registry, name)))
|
|
59
|
+
.sort(codeUnitCompare);
|
|
60
|
+
const outgoing = new Map(nodes.map((name) => [name, new Set()]));
|
|
61
|
+
const indegree = new Map(nodes.map((name) => [name, 0]));
|
|
62
|
+
const issues = [];
|
|
63
|
+
const counts = new Map();
|
|
64
|
+
for (const name of componentNames)
|
|
65
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
66
|
+
for (const [componentName, count] of [...counts].sort(([left], [right]) => codeUnitCompare(left, right))) {
|
|
67
|
+
if (count < 2)
|
|
68
|
+
continue;
|
|
69
|
+
issues.push({
|
|
70
|
+
code: 'duplicate-component',
|
|
71
|
+
componentName,
|
|
72
|
+
componentNames: [componentName],
|
|
73
|
+
count,
|
|
74
|
+
cause: `Component "${componentName}" appears ${count} times on the same entity; component identity must be unique.`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (const declarer of [...present].sort(codeUnitCompare)) {
|
|
78
|
+
const Class = registeredClass(registry, declarer);
|
|
79
|
+
if (Class?.updateAfter !== undefined && !updates(Class)) {
|
|
80
|
+
issues.push({
|
|
81
|
+
code: 'invalid-update-constraint',
|
|
82
|
+
reason: 'passive-declarer',
|
|
83
|
+
declarer,
|
|
84
|
+
componentNames: [declarer],
|
|
85
|
+
cause: `Passive component "${declarer}" declares updateAfter but does not implement onUpdate.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const declarer of nodes) {
|
|
90
|
+
const Class = registeredClass(registry, declarer);
|
|
91
|
+
for (const target of new Set(Class?.updateAfter ?? [])) {
|
|
92
|
+
if (!present.has(target)) {
|
|
93
|
+
if (!registeredClass(registry, target)) {
|
|
94
|
+
issues.push({
|
|
95
|
+
code: 'invalid-update-constraint',
|
|
96
|
+
reason: 'unknown-target',
|
|
97
|
+
declarer,
|
|
98
|
+
target,
|
|
99
|
+
componentNames: [declarer, target],
|
|
100
|
+
cause: `Component "${declarer}" declares updateAfter target "${target}", which is neither present nor registered.`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (target === declarer) {
|
|
106
|
+
issues.push({
|
|
107
|
+
code: 'invalid-update-constraint',
|
|
108
|
+
reason: 'self-edge',
|
|
109
|
+
declarer,
|
|
110
|
+
target,
|
|
111
|
+
componentNames: [declarer],
|
|
112
|
+
cause: `Component "${declarer}" cannot declare itself in updateAfter.`,
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!outgoing.has(target)) {
|
|
117
|
+
issues.push({
|
|
118
|
+
code: 'invalid-update-constraint',
|
|
119
|
+
reason: 'passive-target',
|
|
120
|
+
declarer,
|
|
121
|
+
target,
|
|
122
|
+
componentNames: [declarer, target],
|
|
123
|
+
cause: `Component "${declarer}" declares updateAfter target "${target}", but "${target}" does not implement onUpdate.`,
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const readers = outgoing.get(target);
|
|
128
|
+
if (readers.has(declarer))
|
|
129
|
+
continue;
|
|
130
|
+
readers.add(declarer);
|
|
131
|
+
indegree.set(declarer, (indegree.get(declarer) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const componentNames of updateCycles(nodes, outgoing)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
code: 'component-update-cycle',
|
|
137
|
+
componentNames,
|
|
138
|
+
cause: `Component update cycle among ${componentNames.map((name) => `"${name}"`).join(', ')}.`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (issues.length > 0)
|
|
142
|
+
return { ok: false, issues };
|
|
143
|
+
const ready = nodes.filter((name) => indegree.get(name) === 0);
|
|
144
|
+
const order = [];
|
|
145
|
+
while (ready.length > 0) {
|
|
146
|
+
ready.sort(codeUnitCompare);
|
|
147
|
+
const next = ready.shift();
|
|
148
|
+
order.push(next);
|
|
149
|
+
for (const dependent of [...(outgoing.get(next) ?? [])].sort(codeUnitCompare)) {
|
|
150
|
+
const remaining = (indegree.get(dependent) ?? 0) - 1;
|
|
151
|
+
indegree.set(dependent, remaining);
|
|
152
|
+
if (remaining === 0)
|
|
153
|
+
ready.push(dependent);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, order, issues: [] };
|
|
157
|
+
}
|
package/dist/component.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ export interface ParamSpec {
|
|
|
7
7
|
min?: number;
|
|
8
8
|
max?: number;
|
|
9
9
|
step?: number;
|
|
10
|
-
/** Allowed values for a string param; rendered as a dropdown. */
|
|
10
|
+
/** Allowed values for a string param; rendered as a dropdown. Takes precedence over ref. */
|
|
11
11
|
options?: string[];
|
|
12
|
+
/** Project value this string param names; rendered and validated as a typed reference. */
|
|
13
|
+
ref?: 'prefab' | 'stat' | 'action' | 'clip';
|
|
12
14
|
}
|
|
13
15
|
export interface ComponentClass<T extends Component = Component> {
|
|
14
16
|
new (): T;
|
|
@@ -21,6 +23,14 @@ export interface ComponentClass<T extends Component = Component> {
|
|
|
21
23
|
displayName?: string;
|
|
22
24
|
/** Which properties the inspector exposes, with their ranges. */
|
|
23
25
|
params?: Record<string, ParamSpec>;
|
|
26
|
+
/** Sibling component updates that must complete before this one when present. */
|
|
27
|
+
updateAfter?: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Instance fields holding runtime state rather than authorable defaults.
|
|
30
|
+
* Excluded from authoringDefaults(); a subclass that does not redeclare
|
|
31
|
+
* this inherits its base's list.
|
|
32
|
+
*/
|
|
33
|
+
transient?: readonly string[];
|
|
24
34
|
}
|
|
25
35
|
/** Cardinal unit normal pointing away from the contacted Solid surface. */
|
|
26
36
|
export interface ContactNormal {
|
|
@@ -43,8 +53,12 @@ export declare abstract class Component {
|
|
|
43
53
|
static componentName: string;
|
|
44
54
|
static displayName?: string;
|
|
45
55
|
static params?: Record<string, ParamSpec>;
|
|
56
|
+
static updateAfter?: readonly string[];
|
|
57
|
+
static transient?: readonly string[];
|
|
46
58
|
entity: Entity;
|
|
47
59
|
game: Game;
|
|
60
|
+
/** Replaces automatic runtime-state discovery for Runtime Snapshots. */
|
|
61
|
+
inspectState?(): unknown;
|
|
48
62
|
/** Runs once the component is mounted on its entity. */
|
|
49
63
|
onReady?(): void;
|
|
50
64
|
/** Runs once per frame. */
|
package/dist/component.js
CHANGED
|
@@ -12,6 +12,7 @@ import { type SheetCell, type SheetDef } from '../animation/sheet.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class AnimatedSprite extends Component {
|
|
14
14
|
static componentName: string;
|
|
15
|
+
static updateAfter: readonly string[];
|
|
15
16
|
static params: {
|
|
16
17
|
offsetX: {
|
|
17
18
|
label: string;
|
|
@@ -26,6 +27,7 @@ export declare class AnimatedSprite extends Component {
|
|
|
26
27
|
step: number;
|
|
27
28
|
};
|
|
28
29
|
};
|
|
30
|
+
static transient: string[];
|
|
29
31
|
/** Spritesheet URL. */
|
|
30
32
|
texture: string;
|
|
31
33
|
/** Sheet grid dimensions. */
|
|
@@ -14,11 +14,22 @@ const loader = new THREE.TextureLoader();
|
|
|
14
14
|
*/
|
|
15
15
|
export class AnimatedSprite extends Component {
|
|
16
16
|
static componentName = 'AnimatedSprite';
|
|
17
|
+
static updateAfter = ['StateMachine'];
|
|
17
18
|
static params = {
|
|
18
19
|
offsetX: { label: 'x offset' },
|
|
19
20
|
offsetY: { label: 'y offset' },
|
|
20
21
|
layer: { label: 'layer', min: -5, max: 5, step: 1 },
|
|
21
22
|
};
|
|
23
|
+
static transient = [
|
|
24
|
+
'current',
|
|
25
|
+
'player',
|
|
26
|
+
'sheets',
|
|
27
|
+
'texs',
|
|
28
|
+
'mesh',
|
|
29
|
+
'frame',
|
|
30
|
+
'frameScaleX',
|
|
31
|
+
'frameScaleY',
|
|
32
|
+
];
|
|
22
33
|
/** Spritesheet URL. */
|
|
23
34
|
texture = '';
|
|
24
35
|
/** Sheet grid dimensions. */
|
|
@@ -12,6 +12,7 @@ export class Sprite extends Component {
|
|
|
12
12
|
offsetY: { label: 'y offset' },
|
|
13
13
|
layer: { label: 'layer', min: -5, max: 5, step: 1 },
|
|
14
14
|
};
|
|
15
|
+
static transient = ['mesh'];
|
|
15
16
|
// Size, color and offset are reactive so inspector edits update the live quad.
|
|
16
17
|
// Texture still needs a rebuild. TODO(H1): fully reactive props.
|
|
17
18
|
_width = 1;
|
package/dist/game.d.ts
CHANGED
|
@@ -57,10 +57,12 @@ export declare class Game {
|
|
|
57
57
|
private readonly renderer;
|
|
58
58
|
private readonly resizeObserver;
|
|
59
59
|
private readonly updateFns;
|
|
60
|
+
private readonly invalidUpdateCompositions;
|
|
60
61
|
private readonly resolution;
|
|
61
62
|
private viewHeight;
|
|
62
63
|
private sceneCamera;
|
|
63
64
|
private lastTime;
|
|
65
|
+
private runtimeBridge;
|
|
64
66
|
constructor(options: GameOptions);
|
|
65
67
|
/** Creates a live entity in the scene. */
|
|
66
68
|
spawn(name: string): Entity;
|
|
@@ -88,7 +90,12 @@ export declare class Game {
|
|
|
88
90
|
setViewHeight(value: number): void;
|
|
89
91
|
/** Shuts the game down completely (loop, input, GPU). */
|
|
90
92
|
dispose(): void;
|
|
93
|
+
private resumeRuntime;
|
|
91
94
|
private tick;
|
|
95
|
+
private runFrame;
|
|
96
|
+
private unregisterRuntimeBridge;
|
|
97
|
+
private renderSurface;
|
|
98
|
+
private componentUpdateSchedule;
|
|
92
99
|
private updateSceneCamera;
|
|
93
100
|
private dispatchCollisions;
|
|
94
101
|
private resize;
|
package/dist/game.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { collisionOverlap } from './collision-shape.js';
|
|
3
3
|
import { resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
4
|
+
import { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
4
5
|
import { Hitbox } from './components/hitbox.js';
|
|
5
6
|
import { Entity } from './entity.js';
|
|
6
7
|
import { Emitter } from './events.js';
|
|
7
8
|
import { Input } from './input.js';
|
|
9
|
+
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
10
|
+
import { RuntimeInspector } from './runtime-inspection.js';
|
|
8
11
|
import { registryEntry, spawnFromJson } from './scene.js';
|
|
9
12
|
import { Stats } from './stats.js';
|
|
10
13
|
import { GameUi } from './ui.js';
|
|
@@ -33,10 +36,12 @@ export class Game {
|
|
|
33
36
|
renderer;
|
|
34
37
|
resizeObserver;
|
|
35
38
|
updateFns = new Set();
|
|
39
|
+
invalidUpdateCompositions = new WeakMap();
|
|
36
40
|
resolution;
|
|
37
41
|
viewHeight;
|
|
38
42
|
sceneCamera = null;
|
|
39
43
|
lastTime = 0;
|
|
44
|
+
runtimeBridge = null;
|
|
40
45
|
constructor(options) {
|
|
41
46
|
const { canvas, background = 0x1a1a2e, viewHeight = 10 } = options;
|
|
42
47
|
this.viewHeight = viewHeight;
|
|
@@ -124,6 +129,25 @@ export class Game {
|
|
|
124
129
|
this.setViewHeight(this.sceneCamera.zoom);
|
|
125
130
|
}
|
|
126
131
|
start() {
|
|
132
|
+
const activation = activeRuntimeBridgeHook();
|
|
133
|
+
if (activation) {
|
|
134
|
+
if (!this.runtimeBridge) {
|
|
135
|
+
const inspector = new RuntimeInspector(this);
|
|
136
|
+
this.runtimeBridge = new EngineRuntimeBridge(this.renderer.domElement, activation, {
|
|
137
|
+
step: (dt) => this.runFrame(dt),
|
|
138
|
+
resume: (frame) => this.resumeRuntime(frame),
|
|
139
|
+
pause: () => this.stop(),
|
|
140
|
+
injectAction: (action, operation) => this.input.injectAction(action, operation),
|
|
141
|
+
availableActions: () => this.input.availableActions(),
|
|
142
|
+
heldActions: () => this.input.heldActions(),
|
|
143
|
+
inspect: (metadata, filters) => inspector.snapshot(metadata, filters),
|
|
144
|
+
});
|
|
145
|
+
activation.register(this.runtimeBridge);
|
|
146
|
+
window.addEventListener('pagehide', this.unregisterRuntimeBridge);
|
|
147
|
+
}
|
|
148
|
+
this.renderSurface();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
127
151
|
this.renderer.setAnimationLoop((time) => this.tick(time));
|
|
128
152
|
}
|
|
129
153
|
stop() {
|
|
@@ -146,6 +170,7 @@ export class Game {
|
|
|
146
170
|
/** Shuts the game down completely (loop, input, GPU). */
|
|
147
171
|
dispose() {
|
|
148
172
|
this.stop();
|
|
173
|
+
this.unregisterRuntimeBridge();
|
|
149
174
|
this.input.dispose();
|
|
150
175
|
this.resizeObserver.disconnect();
|
|
151
176
|
this.ui.dispose();
|
|
@@ -153,13 +178,27 @@ export class Game {
|
|
|
153
178
|
entity.destroy();
|
|
154
179
|
this.renderer.dispose();
|
|
155
180
|
}
|
|
181
|
+
resumeRuntime(frame) {
|
|
182
|
+
let previousTime = null;
|
|
183
|
+
this.renderer.setAnimationLoop((time) => {
|
|
184
|
+
const dt = previousTime === null ? 0 : Math.min((time - previousTime) / 1000, 0.1);
|
|
185
|
+
previousTime = time;
|
|
186
|
+
frame(dt);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
156
189
|
tick(time) {
|
|
157
190
|
// Clamp dt: switching tabs or pausing doesn't fast-forward the simulation.
|
|
158
191
|
const dt = Math.min((time - this.lastTime) / 1000, 0.1);
|
|
159
192
|
this.lastTime = time;
|
|
193
|
+
this.runFrame(dt);
|
|
194
|
+
}
|
|
195
|
+
runFrame(dt) {
|
|
160
196
|
if (this.simulate) {
|
|
161
197
|
for (const entity of [...this.entities]) {
|
|
162
|
-
|
|
198
|
+
const schedule = this.componentUpdateSchedule(entity);
|
|
199
|
+
if (!schedule)
|
|
200
|
+
continue;
|
|
201
|
+
for (const component of schedule)
|
|
163
202
|
component.onUpdate?.(dt);
|
|
164
203
|
}
|
|
165
204
|
this.dispatchCollisions();
|
|
@@ -170,6 +209,15 @@ export class Game {
|
|
|
170
209
|
for (const fn of this.updateFns)
|
|
171
210
|
fn(dt);
|
|
172
211
|
this.input.endFrame();
|
|
212
|
+
this.renderSurface();
|
|
213
|
+
}
|
|
214
|
+
unregisterRuntimeBridge = () => {
|
|
215
|
+
window.removeEventListener('pagehide', this.unregisterRuntimeBridge);
|
|
216
|
+
this.runtimeBridge?.unregister();
|
|
217
|
+
this.runtimeBridge = null;
|
|
218
|
+
};
|
|
219
|
+
renderSurface() {
|
|
220
|
+
this.ui.setActive(this.simulate);
|
|
173
221
|
if (this.resolution) {
|
|
174
222
|
// Letterbox bars: clear the whole canvas, then render inside the scissor.
|
|
175
223
|
this.renderer.setScissorTest(false);
|
|
@@ -179,6 +227,36 @@ export class Game {
|
|
|
179
227
|
}
|
|
180
228
|
this.renderer.render(this.scene, this.camera);
|
|
181
229
|
}
|
|
230
|
+
componentUpdateSchedule(entity) {
|
|
231
|
+
const components = [...entity.components];
|
|
232
|
+
const registry = {
|
|
233
|
+
...(this.registry?.components ?? {}),
|
|
234
|
+
};
|
|
235
|
+
const byName = new Map();
|
|
236
|
+
const names = [];
|
|
237
|
+
const signatureParts = [];
|
|
238
|
+
for (const component of components) {
|
|
239
|
+
const Class = component.constructor;
|
|
240
|
+
const name = Class.componentName;
|
|
241
|
+
registry[name] = Class;
|
|
242
|
+
names.push(name);
|
|
243
|
+
byName.set(name, component);
|
|
244
|
+
signatureParts.push(`${name}:${typeof Class.prototype.onUpdate === 'function' ? 'updates' : 'passive'}:` +
|
|
245
|
+
[...new Set(Class.updateAfter ?? [])].sort().join(','));
|
|
246
|
+
}
|
|
247
|
+
const result = resolveComponentUpdateSchedule(names, registry);
|
|
248
|
+
if (!result.ok) {
|
|
249
|
+
const signature = signatureParts.sort().join('|');
|
|
250
|
+
if (this.invalidUpdateCompositions.get(entity) !== signature) {
|
|
251
|
+
this.invalidUpdateCompositions.set(entity, signature);
|
|
252
|
+
console.error(`[waica] invalid component update schedule for "${entity.name}": ` +
|
|
253
|
+
result.issues.map((issue) => issue.cause).join(' '));
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
this.invalidUpdateCompositions.delete(entity);
|
|
258
|
+
return result.order.map((name) => byName.get(name));
|
|
259
|
+
}
|
|
182
260
|
updateSceneCamera(dt) {
|
|
183
261
|
const cam = this.sceneCamera;
|
|
184
262
|
if (!cam)
|
package/dist/index.d.ts
CHANGED
|
@@ -4,11 +4,18 @@ export { CAMERA_DEFAULTS, resolveSceneCamera, stepSceneCamera } from './camera.j
|
|
|
4
4
|
export type { SceneCameraJson, CameraLimitsJson, ResolvedSceneCamera } from './camera.js';
|
|
5
5
|
export { Entity } from './entity.js';
|
|
6
6
|
export { Component } from './component.js';
|
|
7
|
+
export { authoringDefaults } from './authoring-defaults.js';
|
|
7
8
|
export type { ComponentClass, ContactNormal, ParamSpec, SolidContact, } from './component.js';
|
|
8
9
|
export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
|
|
9
10
|
export type { ComponentModule } from './component-registry.js';
|
|
11
|
+
export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
12
|
+
export type { ComponentUpdateCycleIssue, ComponentUpdateRegistry, ComponentUpdateScheduleIssue, ComponentUpdateScheduleResult, DuplicateComponentUpdateIssue, InvalidComponentUpdateConstraintIssue, InvalidComponentUpdateSchedule, InvalidUpdateConstraintReason, ValidComponentUpdateSchedule, } from './component-update-schedule.js';
|
|
10
13
|
export { Input, DEFAULT_BINDINGS } from './input.js';
|
|
11
|
-
export type { ActionName, InputBindings } from './input.js';
|
|
14
|
+
export type { ActionName, InjectedActionOperation, InputBindings } from './input.js';
|
|
15
|
+
export { RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
16
|
+
export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
|
|
17
|
+
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
18
|
+
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotFilters, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
12
19
|
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
|
|
13
20
|
export { Stats } from './stats.js';
|
|
14
21
|
export type { StatValue } from './stats.js';
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,12 @@ export { Game } from './game.js';
|
|
|
2
2
|
export { CAMERA_DEFAULTS, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
3
3
|
export { Entity } from './entity.js';
|
|
4
4
|
export { Component } from './component.js';
|
|
5
|
+
export { authoringDefaults } from './authoring-defaults.js';
|
|
5
6
|
export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
|
|
7
|
+
export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
6
8
|
export { Input, DEFAULT_BINDINGS } from './input.js';
|
|
9
|
+
export { RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
10
|
+
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
7
11
|
export { Stats } from './stats.js';
|
|
8
12
|
export { GameUi } from './ui.js';
|
|
9
13
|
export { Sprite } from './components/sprite.js';
|