@waica/engine 0.18.0 → 0.19.1
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 +22 -0
- 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.d.ts +142 -0
- package/dist/camera-effects.js +338 -0
- 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-time.d.ts +4 -0
- package/dist/game-time.js +2 -1
- package/dist/game.d.ts +6 -0
- package/dist/game.js +59 -17
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- 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 +8 -2
- package/dist/runtime-bridge.js +45 -29
- package/dist/runtime-inspection.d.ts +9 -0
- package/dist/runtime-inspection.js +107 -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 +1 -1
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
|
}
|
|
@@ -5,6 +5,7 @@ import { projectIsometric } from '../projection.js';
|
|
|
5
5
|
import { SOLID_SOURCE_SYMBOL } from '../scene-solids.js';
|
|
6
6
|
import { cellAt as gridCellAt, cellBounds as gridCellBounds, cellIndex as gridCellIndex, } from '../tilemap-grid.js';
|
|
7
7
|
import { Solid } from './solid.js';
|
|
8
|
+
import { reportRejection } from '../report-rejection.js';
|
|
8
9
|
/** One authorable cell map rendered as a single merged geometry. */
|
|
9
10
|
export class Tilemap extends Component {
|
|
10
11
|
static componentName = 'Tilemap';
|
|
@@ -218,14 +219,14 @@ export class Tilemap extends Component {
|
|
|
218
219
|
// happens if the texture was replaced or the component destroyed
|
|
219
220
|
// meanwhile.
|
|
220
221
|
const { texture, settled } = this.game.assets.texture(requested);
|
|
221
|
-
|
|
222
|
+
reportRejection(settled.then((outcome) => {
|
|
222
223
|
if (this.loadedTexture !== texture || this.texture !== requested)
|
|
223
224
|
return;
|
|
224
225
|
if (outcome === 'loaded')
|
|
225
226
|
this.rebuildGeometry();
|
|
226
227
|
else
|
|
227
228
|
this.dropFailedTexture();
|
|
228
|
-
});
|
|
229
|
+
}), 'tilemap texture settle');
|
|
229
230
|
if (this.pixelArt) {
|
|
230
231
|
texture.magFilter = THREE.NearestFilter;
|
|
231
232
|
texture.minFilter = THREE.NearestFilter;
|
|
@@ -260,56 +261,66 @@ export class Tilemap extends Component {
|
|
|
260
261
|
const mesh = this.mesh;
|
|
261
262
|
if (!mesh)
|
|
262
263
|
return;
|
|
263
|
-
const
|
|
264
|
-
const uvs = [];
|
|
265
|
-
const indices = [];
|
|
264
|
+
const buffers = { positions: [], uvs: [], indices: [] };
|
|
266
265
|
const width = Math.max(0, Math.floor(this.mapWidth));
|
|
267
266
|
const height = Math.max(0, Math.floor(this.mapHeight));
|
|
268
267
|
const size = this.cellSize;
|
|
269
268
|
if (Number.isFinite(size) && size > 0) {
|
|
270
|
-
const
|
|
271
|
-
const imageWidth = image?.width && image.width > 0 ? image.width : Math.max(1, this.cols);
|
|
272
|
-
const imageHeight = image?.height && image.height > 0 ? image.height : Math.max(1, this.rows);
|
|
273
|
-
const frameParams = {
|
|
274
|
-
gridOffsetX: this.gridOffsetX,
|
|
275
|
-
gridOffsetY: this.gridOffsetY,
|
|
276
|
-
spacingX: this.spacingX,
|
|
277
|
-
spacingY: this.spacingY,
|
|
278
|
-
cellWidth: this.cellWidth,
|
|
279
|
-
cellHeight: this.cellHeight,
|
|
280
|
-
};
|
|
269
|
+
const sheet = this.sheetSize();
|
|
281
270
|
for (let index = 0; index < width * height; index++) {
|
|
282
271
|
const tile = this.cells[index] ?? -1;
|
|
283
272
|
if (!Number.isFinite(tile) || tile < 0)
|
|
284
273
|
continue;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
const center = this.game.projection === 'isometric'
|
|
290
|
-
? projectIsometric(logicalCenterX, logicalCenterY)
|
|
291
|
-
: { x: logicalCenterX, y: logicalCenterY };
|
|
292
|
-
const halfWidth = this.game.projection === 'isometric' ? size : size / 2;
|
|
293
|
-
const halfHeight = size / 2;
|
|
294
|
-
const z = this.layer * 0.01;
|
|
295
|
-
positions.push(center.x - halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y + halfHeight, z, center.x - halfWidth, center.y + halfHeight, z);
|
|
296
|
-
const frame = sheetCell(imageWidth, imageHeight, this.cols, this.rows, tile, frameParams);
|
|
297
|
-
const left = frame.x / imageWidth;
|
|
298
|
-
const right = (frame.x + frame.width) / imageWidth;
|
|
299
|
-
const bottom = 1 - (frame.y + frame.height) / imageHeight;
|
|
300
|
-
const top = 1 - frame.y / imageHeight;
|
|
301
|
-
uvs.push(left, bottom, right, bottom, right, top, left, top);
|
|
302
|
-
const vertex = positions.length / 3 - 4;
|
|
303
|
-
indices.push(vertex, vertex + 1, vertex + 2, vertex, vertex + 2, vertex + 3);
|
|
274
|
+
this.pushTileQuad(buffers.positions, index % width, Math.floor(index / width));
|
|
275
|
+
buffers.uvs.push(...this.tileUvs(tile, sheet));
|
|
276
|
+
const vertex = buffers.positions.length / 3 - 4;
|
|
277
|
+
buffers.indices.push(vertex, vertex + 1, vertex + 2, vertex, vertex + 2, vertex + 3);
|
|
304
278
|
}
|
|
305
279
|
}
|
|
306
280
|
const geometry = new THREE.BufferGeometry();
|
|
307
|
-
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
308
|
-
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
309
|
-
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
281
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(buffers.positions, 3));
|
|
282
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(buffers.uvs, 2));
|
|
283
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(buffers.indices), 1));
|
|
310
284
|
mesh.geometry.dispose();
|
|
311
285
|
mesh.geometry = geometry;
|
|
312
286
|
}
|
|
287
|
+
/** The loaded sheet image's size, or one pixel per sheet cell until it arrives. */
|
|
288
|
+
sheetSize() {
|
|
289
|
+
const image = this.loadedTexture?.image;
|
|
290
|
+
return {
|
|
291
|
+
width: image?.width && image.width > 0 ? image.width : Math.max(1, this.cols),
|
|
292
|
+
height: image?.height && image.height > 0 ? image.height : Math.max(1, this.rows),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** The four corners of a cell's quad, projected for the Game, on this layer's depth. */
|
|
296
|
+
pushTileQuad(positions, column, row) {
|
|
297
|
+
const size = this.cellSize;
|
|
298
|
+
const logicalCenterX = (column + 0.5) * size;
|
|
299
|
+
const logicalCenterY = (row + 0.5) * size;
|
|
300
|
+
const center = this.game.projection === 'isometric'
|
|
301
|
+
? projectIsometric(logicalCenterX, logicalCenterY)
|
|
302
|
+
: { x: logicalCenterX, y: logicalCenterY };
|
|
303
|
+
const halfWidth = this.game.projection === 'isometric' ? size : size / 2;
|
|
304
|
+
const halfHeight = size / 2;
|
|
305
|
+
const z = this.layer * 0.01;
|
|
306
|
+
positions.push(center.x - halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y + halfHeight, z, center.x - halfWidth, center.y + halfHeight, z);
|
|
307
|
+
}
|
|
308
|
+
/** The UVs of a tile's sheet cell, in the quad's corner order. */
|
|
309
|
+
tileUvs(tile, sheet) {
|
|
310
|
+
const frame = sheetCell(sheet.width, sheet.height, this.cols, this.rows, tile, {
|
|
311
|
+
gridOffsetX: this.gridOffsetX,
|
|
312
|
+
gridOffsetY: this.gridOffsetY,
|
|
313
|
+
spacingX: this.spacingX,
|
|
314
|
+
spacingY: this.spacingY,
|
|
315
|
+
cellWidth: this.cellWidth,
|
|
316
|
+
cellHeight: this.cellHeight,
|
|
317
|
+
});
|
|
318
|
+
const left = frame.x / sheet.width;
|
|
319
|
+
const right = (frame.x + frame.width) / sheet.width;
|
|
320
|
+
const bottom = 1 - (frame.y + frame.height) / sheet.height;
|
|
321
|
+
const top = 1 - frame.y / sheet.height;
|
|
322
|
+
return [left, bottom, right, bottom, right, top, left, top];
|
|
323
|
+
}
|
|
313
324
|
rebuildSolids() {
|
|
314
325
|
if (!this.entity || !this.game)
|
|
315
326
|
return;
|
package/dist/entity.js
CHANGED
|
@@ -22,13 +22,15 @@ export class Entity {
|
|
|
22
22
|
}
|
|
23
23
|
/** Keeps identity scenes zero-copy while projected scenes own a logical transform. */
|
|
24
24
|
setProjected(projected) {
|
|
25
|
-
|
|
26
|
-
return;
|
|
25
|
+
const logical = this.logicalPosition;
|
|
27
26
|
if (projected) {
|
|
28
|
-
|
|
27
|
+
if (logical === null)
|
|
28
|
+
this.logicalPosition = this.node.position.clone();
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
if (logical === null)
|
|
32
|
+
return;
|
|
33
|
+
this.node.position.copy(logical);
|
|
32
34
|
this.logicalPosition = null;
|
|
33
35
|
}
|
|
34
36
|
get scale() {
|
package/dist/game-time.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The closed set of named Penner curves a Tween can ease through. */
|
|
2
2
|
export type EasingName = 'linear' | 'quadIn' | 'quadOut' | 'quadInOut' | 'cubicIn' | 'cubicOut' | 'cubicInOut' | 'sineInOut';
|
|
3
|
+
/** A resolved easing: normalized progress in, eased progress out. */
|
|
4
|
+
type EasingFn = (t: number) => number;
|
|
3
5
|
/** What `after`/`every`/`tween` hand back: see CA-6. */
|
|
4
6
|
export interface TimerHandle {
|
|
5
7
|
/** Idempotent and silent, even on an already-inactive handle. */
|
|
@@ -59,6 +61,8 @@ export interface TweenOptions {
|
|
|
59
61
|
/** Same as `TimerOptions.scope`: `'session'` survives a scene change, anything else means scene. */
|
|
60
62
|
scope?: 'scene' | 'session';
|
|
61
63
|
}
|
|
64
|
+
/** Engine-internal (not re-exported from the package entry): also resolves Camera Effect easings. */
|
|
65
|
+
export declare function resolveEasing(easing: EasingName | ((t: number) => number) | undefined): EasingFn | null;
|
|
62
66
|
/**
|
|
63
67
|
* Module-private key for the per-step advance pass. Not exported, so
|
|
64
68
|
* `time[ADVANCE]()` cannot be spelled outside this file — `advanceGameTime`
|
package/dist/game-time.js
CHANGED
|
@@ -15,7 +15,8 @@ const EASINGS = {
|
|
|
15
15
|
cubicInOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - ((-2 * t + 2) ** 3) / 2),
|
|
16
16
|
sineInOut: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
17
17
|
};
|
|
18
|
-
|
|
18
|
+
/** Engine-internal (not re-exported from the package entry): also resolves Camera Effect easings. */
|
|
19
|
+
export function resolveEasing(easing) {
|
|
19
20
|
if (easing === undefined)
|
|
20
21
|
return EASINGS.linear;
|
|
21
22
|
if (typeof easing === 'function')
|