@waica/engine 0.19.0 → 0.19.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.
- package/dist/animation/sheet.js +9 -3
- package/dist/assets/asset-loader.js +3 -2
- package/dist/audio/web-audio-backend.js +15 -6
- package/dist/authoring-defaults.js +4 -4
- package/dist/camera-effects.js +7 -3
- package/dist/collision-shape.js +45 -20
- package/dist/component-registry.d.ts +11 -1
- package/dist/component-registry.js +26 -3
- package/dist/component-update-schedule.d.ts +2 -0
- package/dist/component-update-schedule.js +124 -68
- package/dist/components/animated-sprite.js +3 -2
- package/dist/components/sprite.js +3 -2
- package/dist/components/tilemap.d.ts +6 -0
- package/dist/components/tilemap.js +49 -38
- package/dist/entity.js +6 -4
- package/dist/game.js +23 -9
- package/dist/pointer.js +27 -10
- package/dist/render-sort.js +11 -5
- package/dist/report-rejection.d.ts +7 -0
- package/dist/report-rejection.js +12 -0
- package/dist/runtime-bridge.d.ts +4 -0
- package/dist/runtime-bridge.js +35 -27
- package/dist/runtime-inspection.js +106 -86
- package/dist/scene.d.ts +0 -1
- package/dist/scene.js +34 -1
- package/dist/spatial-broadphase.js +60 -34
- package/dist/spatial-query-geometry.js +48 -35
- package/package.json +3 -3
package/dist/animation/sheet.js
CHANGED
|
@@ -11,11 +11,13 @@ export function sheetFrameCount(sheet) {
|
|
|
11
11
|
*/
|
|
12
12
|
export function locateFrame(sheets, frame) {
|
|
13
13
|
let rest = Math.max(0, Math.floor(frame));
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
let i = 0;
|
|
15
|
+
for (const sheet of sheets) {
|
|
16
|
+
const count = sheetFrameCount(sheet);
|
|
16
17
|
if (rest < count || i === sheets.length - 1)
|
|
17
18
|
return { sheet: i, frame: Math.min(rest, count - 1) };
|
|
18
19
|
rest -= count;
|
|
20
|
+
i++;
|
|
19
21
|
}
|
|
20
22
|
return { sheet: 0, frame: 0 };
|
|
21
23
|
}
|
|
@@ -28,7 +30,11 @@ const positive = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
|
28
30
|
export function sheetCell(imageWidth, imageHeight, cols, rows, frame, params = {}) {
|
|
29
31
|
if (params.cells?.length) {
|
|
30
32
|
const index = Math.min(Math.max(0, Math.floor(frame)), params.cells.length - 1);
|
|
31
|
-
|
|
33
|
+
const cell = params.cells[index];
|
|
34
|
+
// Invariant: the index is clamped into a non-empty cells array.
|
|
35
|
+
if (!cell)
|
|
36
|
+
throw new Error(`sheetCell: missing cell at clamped index ${index}`);
|
|
37
|
+
return cell;
|
|
32
38
|
}
|
|
33
39
|
const c = Math.max(1, Math.floor(cols));
|
|
34
40
|
const r = Math.max(1, Math.floor(rows));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { ThreeTextureBackend } from './texture-backend.js';
|
|
3
|
+
import { reportRejection } from '../report-rejection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Not `base.clone()`: `Texture.copy` sets `needsUpdate`, which bumps the
|
|
5
6
|
* shared Source's version — a GPU re-upload of the same image for every
|
|
@@ -52,10 +53,10 @@ export class AssetLoader {
|
|
|
52
53
|
if (entry.outcome === null) {
|
|
53
54
|
// Chained here, before the consumer chains its own continuation on
|
|
54
55
|
// `settled`, so the clone is uploadable by the time that one runs.
|
|
55
|
-
|
|
56
|
+
reportRejection(entry.settled.then((outcome) => {
|
|
56
57
|
if (outcome === 'loaded')
|
|
57
58
|
texture.version = entry.base.version;
|
|
58
|
-
});
|
|
59
|
+
}), 'texture upload');
|
|
59
60
|
}
|
|
60
61
|
return { texture, settled: entry.settled };
|
|
61
62
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportRejection } from '../report-rejection.js';
|
|
1
2
|
/**
|
|
2
3
|
* The real WebAudio implementation (ADR 0013's default). Builds a small
|
|
3
4
|
* mixing graph — one GainNode per sound feeding a StereoPannerNode, feeding
|
|
@@ -90,13 +91,19 @@ export class WebAudioBackend {
|
|
|
90
91
|
this.masterGain.gain.value = volume;
|
|
91
92
|
}
|
|
92
93
|
suspend() {
|
|
93
|
-
|
|
94
|
+
const suspending = this.context?.suspend();
|
|
95
|
+
if (suspending)
|
|
96
|
+
reportRejection(suspending, 'audio suspend');
|
|
94
97
|
}
|
|
95
98
|
resume() {
|
|
96
|
-
|
|
99
|
+
const resuming = this.ensureContext()?.resume();
|
|
100
|
+
if (resuming)
|
|
101
|
+
reportRejection(resuming, 'audio resume');
|
|
97
102
|
}
|
|
98
103
|
close() {
|
|
99
|
-
|
|
104
|
+
const closing = this.context?.close();
|
|
105
|
+
if (closing)
|
|
106
|
+
reportRejection(closing, 'audio close');
|
|
100
107
|
this.context = null;
|
|
101
108
|
this.masterGain = null;
|
|
102
109
|
this.channelGains.clear();
|
|
@@ -120,11 +127,13 @@ export class WebAudioBackend {
|
|
|
120
127
|
const existing = this.channelGains.get(channel);
|
|
121
128
|
if (existing)
|
|
122
129
|
return existing;
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
const { context, masterGain } = this;
|
|
131
|
+
// Invariant: only called once ensureContext() has produced the context and master gain.
|
|
132
|
+
if (!context || !masterGain)
|
|
133
|
+
throw new Error('ensureChannelGain called before ensureContext');
|
|
125
134
|
const gain = context.createGain();
|
|
126
135
|
gain.gain.value = this.effectiveChannelGain(channel);
|
|
127
|
-
gain.connect(
|
|
136
|
+
gain.connect(masterGain);
|
|
128
137
|
this.channelGains.set(channel, gain);
|
|
129
138
|
return gain;
|
|
130
139
|
}
|
|
@@ -5,11 +5,11 @@ function isSerializable(value) {
|
|
|
5
5
|
const kind = typeof value;
|
|
6
6
|
if (kind === 'string' || kind === 'number' || kind === 'boolean')
|
|
7
7
|
return true;
|
|
8
|
-
if (
|
|
8
|
+
if (typeof value !== 'object')
|
|
9
9
|
return false;
|
|
10
10
|
if (Array.isArray(value))
|
|
11
11
|
return true;
|
|
12
|
-
const proto =
|
|
12
|
+
const proto = Reflect.getPrototypeOf(value);
|
|
13
13
|
return proto === Object.prototype || proto === null;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
@@ -32,7 +32,7 @@ export function authoringDefaults(Class, onError) {
|
|
|
32
32
|
}
|
|
33
33
|
const transient = new Set(Class.transient ?? []);
|
|
34
34
|
const keys = new Set(Object.keys(instance));
|
|
35
|
-
for (let proto =
|
|
35
|
+
for (let proto = Reflect.getPrototypeOf(instance); proto && proto !== Object.prototype; proto = Reflect.getPrototypeOf(proto)) {
|
|
36
36
|
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(proto))) {
|
|
37
37
|
if (descriptor.set)
|
|
38
38
|
keys.add(key);
|
|
@@ -42,7 +42,7 @@ export function authoringDefaults(Class, onError) {
|
|
|
42
42
|
for (const key of keys) {
|
|
43
43
|
if (EXCLUDED_KEYS.has(key) || key.startsWith('_') || transient.has(key))
|
|
44
44
|
continue;
|
|
45
|
-
const value = instance
|
|
45
|
+
const value = Reflect.get(instance, key);
|
|
46
46
|
if (value === undefined || !isSerializable(value))
|
|
47
47
|
continue;
|
|
48
48
|
result[key] = value;
|
package/dist/camera-effects.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
|
|
2
2
|
import { resolveEasing } from './game-time.js';
|
|
3
|
-
const COLORS =
|
|
3
|
+
const COLORS = new Map([
|
|
4
|
+
['black', '#000000'],
|
|
5
|
+
['white', '#ffffff'],
|
|
6
|
+
]);
|
|
4
7
|
/** Above the UI overlay (`ui.ts`, z-index 9000): a Fade covers the HUD too. */
|
|
5
8
|
const FADE_Z = 9001;
|
|
6
9
|
const FLASH_Z = 9002;
|
|
@@ -9,8 +12,9 @@ const LAYOUT = Symbol('waica.cameraEffects.layout');
|
|
|
9
12
|
function normalizeColor(value) {
|
|
10
13
|
if (typeof value !== 'string')
|
|
11
14
|
return null;
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
const named = COLORS.get(value);
|
|
16
|
+
if (named !== undefined)
|
|
17
|
+
return named;
|
|
14
18
|
return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toLowerCase() : null;
|
|
15
19
|
}
|
|
16
20
|
function validSeconds(value) {
|
package/dist/collision-shape.js
CHANGED
|
@@ -74,34 +74,57 @@ export function collisionOverlap(a, b) {
|
|
|
74
74
|
}
|
|
75
75
|
const verticesA = collisionVertices(a);
|
|
76
76
|
const verticesB = collisionVertices(b);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const a2 = verticesA[(ai + 1) % verticesA.length];
|
|
80
|
-
for (let bi = 0; bi < verticesB.length; bi++) {
|
|
81
|
-
const b1 = verticesB[bi];
|
|
82
|
-
const b2 = verticesB[(bi + 1) % verticesB.length];
|
|
83
|
-
if (segmentsCross(a1, a2, b1, b2))
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
77
|
+
if (polygonEdgesCross(verticesA, verticesB))
|
|
78
|
+
return true;
|
|
87
79
|
return hasInteriorPoint(verticesA, verticesB) || hasInteriorPoint(verticesB, verticesA);
|
|
88
80
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
/** Whether any closed-polygon edge of `polygonA` properly crosses one of `polygonB`. */
|
|
82
|
+
function polygonEdgesCross(polygonA, polygonB) {
|
|
83
|
+
let a1 = polygonA.at(-1);
|
|
84
|
+
if (a1 === undefined)
|
|
85
|
+
return false;
|
|
86
|
+
for (const a2 of polygonA) {
|
|
87
|
+
if (edgeCrossesPolygon(a1, a2, polygonB))
|
|
93
88
|
return true;
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
a1 = a2;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function edgeCrossesPolygon(a1, a2, polygon) {
|
|
94
|
+
let b1 = polygon.at(-1);
|
|
95
|
+
if (b1 === undefined)
|
|
96
|
+
return false;
|
|
97
|
+
for (const b2 of polygon) {
|
|
98
|
+
if (segmentsCross(a1, a2, b1, b2))
|
|
96
99
|
return true;
|
|
97
|
-
|
|
100
|
+
b1 = b2;
|
|
98
101
|
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
function hasInteriorPoint(points, polygon) {
|
|
105
|
+
if (hasVertexOrMidpointInside(points, polygon))
|
|
106
|
+
return true;
|
|
99
107
|
const center = [
|
|
100
108
|
points.reduce((sum, [x]) => sum + x, 0) / points.length,
|
|
101
109
|
points.reduce((sum, [, y]) => sum + y, 0) / points.length,
|
|
102
110
|
];
|
|
103
111
|
return pointInPolygon(center, polygon);
|
|
104
112
|
}
|
|
113
|
+
/** Whether any vertex of `points`, or the midpoint of any of its closed-polygon edges, lies inside `polygon`. */
|
|
114
|
+
function hasVertexOrMidpointInside(points, polygon) {
|
|
115
|
+
let previous = points.at(-1);
|
|
116
|
+
if (previous === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
for (const point of points) {
|
|
119
|
+
if (pointInPolygon(point, polygon))
|
|
120
|
+
return true;
|
|
121
|
+
if (pointInPolygon([(previous[0] + point[0]) / 2, (previous[1] + point[1]) / 2], polygon)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
previous = point;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
105
128
|
function segmentsCross(a1, a2, b1, b2) {
|
|
106
129
|
const ab1 = cross(a1, a2, b1);
|
|
107
130
|
const ab2 = cross(a1, a2, b2);
|
|
@@ -114,15 +137,17 @@ function cross(a, b, point) {
|
|
|
114
137
|
}
|
|
115
138
|
function pointInPolygon(point, polygon) {
|
|
116
139
|
let inside = false;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
let b = polygon.at(-1);
|
|
141
|
+
if (b === undefined)
|
|
142
|
+
return inside;
|
|
143
|
+
for (const a of polygon) {
|
|
120
144
|
if (pointOnSegment(point, a, b))
|
|
121
145
|
return false;
|
|
122
146
|
const crosses = (a[1] > point[1]) !== (b[1] > point[1]) &&
|
|
123
147
|
point[0] < ((b[0] - a[0]) * (point[1] - a[1])) / (b[1] - a[1]) + a[0];
|
|
124
148
|
if (crosses)
|
|
125
149
|
inside = !inside;
|
|
150
|
+
b = a;
|
|
126
151
|
}
|
|
127
152
|
return inside;
|
|
128
153
|
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { type ComponentClass } from './component.js';
|
|
1
|
+
import { Component, type ComponentClass } from './component.js';
|
|
2
2
|
import type { SceneRegistry } from './scene.js';
|
|
3
3
|
/** The namespace returned by importing one project code module. */
|
|
4
4
|
export type ComponentModule = Readonly<Record<string, unknown>>;
|
|
5
|
+
/** Whether `value` is a class that extends Component (the abstract base itself is not). */
|
|
6
|
+
export declare function isComponentClass(value: unknown): value is ComponentClass;
|
|
7
|
+
/**
|
|
8
|
+
* The class a live component was constructed from. TypeScript types
|
|
9
|
+
* `constructor` as Function; this narrows it for real instead of asserting.
|
|
10
|
+
* It checks the shape (a constructor with a static componentName), not
|
|
11
|
+
* `instanceof Component`, so a project that resolves a second copy of
|
|
12
|
+
* @waica/engine keeps working. Internal: not re-exported from the package entry.
|
|
13
|
+
*/
|
|
14
|
+
export declare function componentClassOf(component: Component): ComponentClass;
|
|
5
15
|
/**
|
|
6
16
|
* Finds exported Component subclasses in project modules. Classes are keyed by
|
|
7
17
|
* their stable componentName, not by the export name, so minification and
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import { Component } from './component.js';
|
|
2
|
+
/** Whether `value` is a class that extends Component (the abstract base itself is not). */
|
|
3
|
+
export function isComponentClass(value) {
|
|
4
|
+
if (typeof value !== 'function')
|
|
5
|
+
return false;
|
|
6
|
+
const prototype = Reflect.get(value, 'prototype');
|
|
7
|
+
return prototype instanceof Component;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The class a live component was constructed from. TypeScript types
|
|
11
|
+
* `constructor` as Function; this narrows it for real instead of asserting.
|
|
12
|
+
* It checks the shape (a constructor with a static componentName), not
|
|
13
|
+
* `instanceof Component`, so a project that resolves a second copy of
|
|
14
|
+
* @waica/engine keeps working. Internal: not re-exported from the package entry.
|
|
15
|
+
*/
|
|
16
|
+
export function componentClassOf(component) {
|
|
17
|
+
const Class = component.constructor;
|
|
18
|
+
if (!hasComponentName(Class)) {
|
|
19
|
+
throw new TypeError('a component was not constructed from a Component class');
|
|
20
|
+
}
|
|
21
|
+
return Class;
|
|
22
|
+
}
|
|
23
|
+
/** A constructor carrying the static componentName every Component class declares. */
|
|
24
|
+
function hasComponentName(value) {
|
|
25
|
+
return typeof value === 'function' && typeof Reflect.get(value, 'componentName') === 'string';
|
|
26
|
+
}
|
|
2
27
|
/**
|
|
3
28
|
* Finds exported Component subclasses in project modules. Classes are keyed by
|
|
4
29
|
* their stable componentName, not by the export name, so minification and
|
|
@@ -8,11 +33,9 @@ export function collectModuleComponents(modules, warn = console.warn) {
|
|
|
8
33
|
const components = {};
|
|
9
34
|
for (const module of modules) {
|
|
10
35
|
for (const value of Object.values(module)) {
|
|
11
|
-
if (
|
|
36
|
+
if (!isComponentClass(value))
|
|
12
37
|
continue;
|
|
13
38
|
const Class = value;
|
|
14
|
-
if (!(Class.prototype instanceof Component))
|
|
15
|
-
continue;
|
|
16
39
|
// Without its own componentName a class inherits the base's, so nothing
|
|
17
40
|
// could reference it from scene JSON. Silently skipping it looks like
|
|
18
41
|
// the editor lost the file: say so instead.
|
|
@@ -32,6 +32,8 @@ export interface InvalidComponentUpdateSchedule {
|
|
|
32
32
|
}
|
|
33
33
|
export type ComponentUpdateScheduleResult = ValidComponentUpdateSchedule | InvalidComponentUpdateSchedule;
|
|
34
34
|
export type ComponentUpdateRegistry = Readonly<Record<string, ComponentClass | undefined>>;
|
|
35
|
+
/** Whether instances of `Class` run each frame (its prototype defines onUpdate). */
|
|
36
|
+
export declare function implementsOnUpdate(Class: ComponentClass | undefined): boolean;
|
|
35
37
|
/**
|
|
36
38
|
* Resolves one entity's deterministic component update schedule without
|
|
37
39
|
* constructing components or mutating either input.
|
|
@@ -4,8 +4,26 @@ function codeUnitCompare(left, right) {
|
|
|
4
4
|
function registeredClass(registry, componentName) {
|
|
5
5
|
return Object.hasOwn(registry, componentName) ? registry[componentName] : undefined;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/** Whether instances of `Class` run each frame (its prototype defines onUpdate). */
|
|
8
|
+
export function implementsOnUpdate(Class) {
|
|
9
|
+
if (!Class)
|
|
10
|
+
return false;
|
|
11
|
+
const prototype = Reflect.get(Class, 'prototype');
|
|
12
|
+
return typeof prototype === 'object' && prototype !== null && typeof Reflect.get(prototype, 'onUpdate') === 'function';
|
|
13
|
+
}
|
|
14
|
+
/** Reads a Tarjan bookkeeping entry that the traversal has already recorded. */
|
|
15
|
+
function recorded(map, node, what) {
|
|
16
|
+
const value = map.get(node);
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
throw new Error(`Component update cycle search has no ${what} for "${node}"`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/** The first member of a strongly connected group, which always has at least two members. */
|
|
22
|
+
function firstMember(group) {
|
|
23
|
+
const [first] = group;
|
|
24
|
+
if (first === undefined)
|
|
25
|
+
throw new Error('Component update cycle group is empty');
|
|
26
|
+
return first;
|
|
9
27
|
}
|
|
10
28
|
function updateCycles(nodes, outgoing) {
|
|
11
29
|
let nextIndex = 0;
|
|
@@ -23,21 +41,22 @@ function updateCycles(nodes, outgoing) {
|
|
|
23
41
|
for (const dependent of [...(outgoing.get(node) ?? [])].sort(codeUnitCompare)) {
|
|
24
42
|
if (!indices.has(dependent)) {
|
|
25
43
|
visit(dependent);
|
|
26
|
-
lowLinks.set(node, Math.min(lowLinks
|
|
44
|
+
lowLinks.set(node, Math.min(recorded(lowLinks, node, 'low link'), recorded(lowLinks, dependent, 'low link')));
|
|
27
45
|
}
|
|
28
46
|
else if (onStack.has(dependent)) {
|
|
29
|
-
lowLinks.set(node, Math.min(lowLinks
|
|
47
|
+
lowLinks.set(node, Math.min(recorded(lowLinks, node, 'low link'), recorded(indices, dependent, 'index')));
|
|
30
48
|
}
|
|
31
49
|
}
|
|
32
50
|
if (lowLinks.get(node) !== indices.get(node))
|
|
33
51
|
return;
|
|
34
52
|
const group = [];
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
let member = stack.pop();
|
|
54
|
+
while (member !== undefined) {
|
|
37
55
|
onStack.delete(member);
|
|
38
56
|
group.push(member);
|
|
39
57
|
if (member === node)
|
|
40
58
|
break;
|
|
59
|
+
member = stack.pop();
|
|
41
60
|
}
|
|
42
61
|
if (group.length > 1)
|
|
43
62
|
cycles.push(group.sort(codeUnitCompare));
|
|
@@ -46,19 +65,9 @@ function updateCycles(nodes, outgoing) {
|
|
|
46
65
|
if (!indices.has(node))
|
|
47
66
|
visit(node);
|
|
48
67
|
}
|
|
49
|
-
return cycles.sort((left, right) => codeUnitCompare(left
|
|
68
|
+
return cycles.sort((left, right) => codeUnitCompare(firstMember(left), firstMember(right)));
|
|
50
69
|
}
|
|
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]));
|
|
70
|
+
function duplicateComponentIssues(componentNames) {
|
|
62
71
|
const issues = [];
|
|
63
72
|
const counts = new Map();
|
|
64
73
|
for (const name of componentNames)
|
|
@@ -74,9 +83,13 @@ export function resolveComponentUpdateSchedule(componentNames, registry) {
|
|
|
74
83
|
cause: `Component "${componentName}" appears ${count} times on the same entity; component identity must be unique.`,
|
|
75
84
|
});
|
|
76
85
|
}
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
function passiveDeclarerIssues({ present, registry }) {
|
|
89
|
+
const issues = [];
|
|
77
90
|
for (const declarer of [...present].sort(codeUnitCompare)) {
|
|
78
91
|
const Class = registeredClass(registry, declarer);
|
|
79
|
-
if (Class?.updateAfter !== undefined && !
|
|
92
|
+
if (Class?.updateAfter !== undefined && !implementsOnUpdate(Class)) {
|
|
80
93
|
issues.push({
|
|
81
94
|
code: 'invalid-update-constraint',
|
|
82
95
|
reason: 'passive-declarer',
|
|
@@ -86,65 +99,75 @@ export function resolveComponentUpdateSchedule(componentNames, registry) {
|
|
|
86
99
|
});
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
return issues;
|
|
103
|
+
}
|
|
104
|
+
/** Adds every valid updateAfter edge to the graph and returns the constraints that are not. */
|
|
105
|
+
function linkUpdateConstraints(graph, scope) {
|
|
106
|
+
const issues = [];
|
|
107
|
+
for (const declarer of graph.nodes) {
|
|
108
|
+
const Class = registeredClass(scope.registry, declarer);
|
|
91
109
|
for (const target of new Set(Class?.updateAfter ?? [])) {
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
110
|
+
const verdict = constraintVerdict({ declarer, target }, graph, scope);
|
|
111
|
+
if (verdict === 'ignore')
|
|
103
112
|
continue;
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
});
|
|
113
|
+
if (verdict !== 'link') {
|
|
114
|
+
issues.push(verdict);
|
|
125
115
|
continue;
|
|
126
116
|
}
|
|
127
|
-
const readers = outgoing.get(target);
|
|
128
|
-
if (readers.has(declarer))
|
|
117
|
+
const readers = graph.outgoing.get(target);
|
|
118
|
+
if (!readers || readers.has(declarer))
|
|
129
119
|
continue;
|
|
130
120
|
readers.add(declarer);
|
|
131
|
-
indegree.set(declarer, (indegree.get(declarer) ?? 0) + 1);
|
|
121
|
+
graph.indegree.set(declarer, (graph.indegree.get(declarer) ?? 0) + 1);
|
|
132
122
|
}
|
|
133
123
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
return issues;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether `declarer` may update after `target`: an edge to link, a
|
|
128
|
+
* registered-but-absent target to ignore, or the invalid-constraint issue.
|
|
129
|
+
*/
|
|
130
|
+
function constraintVerdict({ declarer, target }, graph, { present, registry }) {
|
|
131
|
+
if (!present.has(target)) {
|
|
132
|
+
if (registeredClass(registry, target))
|
|
133
|
+
return 'ignore';
|
|
134
|
+
return {
|
|
135
|
+
code: 'invalid-update-constraint',
|
|
136
|
+
reason: 'unknown-target',
|
|
137
|
+
declarer,
|
|
138
|
+
target,
|
|
139
|
+
componentNames: [declarer, target],
|
|
140
|
+
cause: `Component "${declarer}" declares updateAfter target "${target}", which is neither present nor registered.`,
|
|
141
|
+
};
|
|
140
142
|
}
|
|
141
|
-
if (
|
|
142
|
-
return {
|
|
143
|
+
if (target === declarer) {
|
|
144
|
+
return {
|
|
145
|
+
code: 'invalid-update-constraint',
|
|
146
|
+
reason: 'self-edge',
|
|
147
|
+
declarer,
|
|
148
|
+
target,
|
|
149
|
+
componentNames: [declarer],
|
|
150
|
+
cause: `Component "${declarer}" cannot declare itself in updateAfter.`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (graph.outgoing.has(target))
|
|
154
|
+
return 'link';
|
|
155
|
+
return {
|
|
156
|
+
code: 'invalid-update-constraint',
|
|
157
|
+
reason: 'passive-target',
|
|
158
|
+
declarer,
|
|
159
|
+
target,
|
|
160
|
+
componentNames: [declarer, target],
|
|
161
|
+
cause: `Component "${declarer}" declares updateAfter target "${target}", but "${target}" does not implement onUpdate.`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Kahn's order over the graph, always taking the code-unit-smallest ready component first. */
|
|
165
|
+
function updateOrder({ nodes, outgoing, indegree }) {
|
|
143
166
|
const ready = nodes.filter((name) => indegree.get(name) === 0);
|
|
144
167
|
const order = [];
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
ready.sort(codeUnitCompare);
|
|
169
|
+
let next = ready.shift();
|
|
170
|
+
while (next !== undefined) {
|
|
148
171
|
order.push(next);
|
|
149
172
|
for (const dependent of [...(outgoing.get(next) ?? [])].sort(codeUnitCompare)) {
|
|
150
173
|
const remaining = (indegree.get(dependent) ?? 0) - 1;
|
|
@@ -152,6 +175,39 @@ export function resolveComponentUpdateSchedule(componentNames, registry) {
|
|
|
152
175
|
if (remaining === 0)
|
|
153
176
|
ready.push(dependent);
|
|
154
177
|
}
|
|
178
|
+
ready.sort(codeUnitCompare);
|
|
179
|
+
next = ready.shift();
|
|
155
180
|
}
|
|
156
|
-
return
|
|
181
|
+
return order;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolves one entity's deterministic component update schedule without
|
|
185
|
+
* constructing components or mutating either input.
|
|
186
|
+
*/
|
|
187
|
+
export function resolveComponentUpdateSchedule(componentNames, registry) {
|
|
188
|
+
const present = new Set(componentNames);
|
|
189
|
+
const scope = { present, registry };
|
|
190
|
+
const nodes = [...present]
|
|
191
|
+
.filter((name) => implementsOnUpdate(registeredClass(registry, name)))
|
|
192
|
+
.sort(codeUnitCompare);
|
|
193
|
+
const graph = {
|
|
194
|
+
nodes,
|
|
195
|
+
outgoing: new Map(nodes.map((name) => [name, new Set()])),
|
|
196
|
+
indegree: new Map(nodes.map((name) => [name, 0])),
|
|
197
|
+
};
|
|
198
|
+
const issues = [
|
|
199
|
+
...duplicateComponentIssues(componentNames),
|
|
200
|
+
...passiveDeclarerIssues(scope),
|
|
201
|
+
...linkUpdateConstraints(graph, scope),
|
|
202
|
+
];
|
|
203
|
+
for (const cycle of updateCycles(nodes, graph.outgoing)) {
|
|
204
|
+
issues.push({
|
|
205
|
+
code: 'component-update-cycle',
|
|
206
|
+
componentNames: cycle,
|
|
207
|
+
cause: `Component update cycle among ${cycle.map((name) => `"${name}"`).join(', ')}.`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (issues.length > 0)
|
|
211
|
+
return { ok: false, issues };
|
|
212
|
+
return { ok: true, order: updateOrder(graph), issues: [] };
|
|
157
213
|
}
|
|
@@ -3,6 +3,7 @@ import { Component } from '../component.js';
|
|
|
3
3
|
import { ClipPlayer } from '../animation/clip-player.js';
|
|
4
4
|
import { locateFrame, sheetCell } from '../animation/sheet.js';
|
|
5
5
|
import { spritePlacement } from '../sprite-placement.js';
|
|
6
|
+
import { reportRejection } from '../report-rejection.js';
|
|
6
7
|
const clampAnchor = (value) => Math.min(1, Math.max(0, value));
|
|
7
8
|
/**
|
|
8
9
|
* Sprite animated from one or more spritesheets. The main sheet is the
|
|
@@ -215,13 +216,13 @@ export class AnimatedSprite extends Component {
|
|
|
215
216
|
if (!url)
|
|
216
217
|
return new THREE.Texture();
|
|
217
218
|
const { texture, settled } = this.game.assets.texture(url);
|
|
218
|
-
|
|
219
|
+
reportRejection(settled.then((outcome) => {
|
|
219
220
|
if (!this.texs.includes(texture))
|
|
220
221
|
return;
|
|
221
222
|
if (outcome === 'failed')
|
|
222
223
|
this.failedSheets.add(texture);
|
|
223
224
|
this.applyFrame();
|
|
224
|
-
});
|
|
225
|
+
}), 'sheet texture settle');
|
|
225
226
|
return texture;
|
|
226
227
|
}
|
|
227
228
|
showFrame(index) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { Component } from '../component.js';
|
|
3
3
|
import { spritePlacement } from '../sprite-placement.js';
|
|
4
|
+
import { reportRejection } from '../report-rejection.js';
|
|
4
5
|
const clampAnchor = (value) => Math.min(1, Math.max(0, value));
|
|
5
6
|
/**
|
|
6
7
|
* Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
|
|
@@ -118,10 +119,10 @@ export class Sprite extends Component {
|
|
|
118
119
|
}
|
|
119
120
|
material.map = texture;
|
|
120
121
|
material.color.set(0xffffff);
|
|
121
|
-
|
|
122
|
+
reportRejection(settled.then((outcome) => {
|
|
122
123
|
if (outcome === 'failed')
|
|
123
124
|
this.dropFailedTexture(texture);
|
|
124
|
-
});
|
|
125
|
+
}), 'sprite texture settle');
|
|
125
126
|
}
|
|
126
127
|
this.mesh = new THREE.Mesh(this.createGeometry(), material);
|
|
127
128
|
this.mesh.position.z = this.layer * 0.01;
|
|
@@ -115,5 +115,11 @@ export declare class Tilemap extends Component implements SolidSource {
|
|
|
115
115
|
private dropFailedTexture;
|
|
116
116
|
private makeMaterial;
|
|
117
117
|
private rebuildGeometry;
|
|
118
|
+
/** The loaded sheet image's size, or one pixel per sheet cell until it arrives. */
|
|
119
|
+
private sheetSize;
|
|
120
|
+
/** The four corners of a cell's quad, projected for the Game, on this layer's depth. */
|
|
121
|
+
private pushTileQuad;
|
|
122
|
+
/** The UVs of a tile's sheet cell, in the quad's corner order. */
|
|
123
|
+
private tileUvs;
|
|
118
124
|
private rebuildSolids;
|
|
119
125
|
}
|