@skewedaspect/sage-vue 0.9.6 → 0.10.0-beta.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/{sageCanvas.vue.d.ts → components/sageCanvas.vue.d.ts} +3 -3
- package/dist/{useSageEngine.d.ts → composables/useSageEngine.d.ts} +1 -1
- package/dist/composables/useSageEntities.d.ts +13 -0
- package/dist/composables/useSageEntity.d.ts +23 -0
- package/dist/composables/useSageEvent.d.ts +4 -0
- package/dist/{useSageTimer.d.ts → composables/useSageTimer.d.ts} +4 -3
- package/dist/composables/viewSync.d.ts +39 -0
- package/dist/index.d.ts +8 -6
- package/dist/sage-vue.css +1 -1
- package/dist/sage-vue.js +200 -68
- package/dist/sage-vue.js.map +1 -1
- package/dist/symbols.d.ts +1 -1
- package/package.json +7 -3
- package/dist/useSageEvent.d.ts +0 -4
- package/dist/useSageState.d.ts +0 -6
- /package/dist/{useSageAction.d.ts → composables/useSageAction.d.ts} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { GameEngine,
|
|
1
|
+
import { GameEngine, GameEntityType, SageOptions } from '@skewedaspect/sage';
|
|
2
2
|
type __VLS_Props = {
|
|
3
|
-
|
|
3
|
+
entityClasses?: Readonly<Record<string, GameEntityType>>;
|
|
4
4
|
options?: SageOptions;
|
|
5
5
|
engine?: GameEngine;
|
|
6
6
|
};
|
|
@@ -27,8 +27,8 @@ declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}
|
|
|
27
27
|
"onEngine-ready"?: ((engine: GameEngine) => any) | undefined;
|
|
28
28
|
"onEngine-error"?: ((error: unknown) => any) | undefined;
|
|
29
29
|
}>, {
|
|
30
|
+
entityClasses: Readonly<Record<string, GameEntityType>>;
|
|
30
31
|
options: SageOptions;
|
|
31
|
-
entityDefinitions: GameEntityDefinition[];
|
|
32
32
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
|
33
33
|
containerRef: HTMLDivElement;
|
|
34
34
|
canvasRef: HTMLCanvasElement;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComputedRef, ShallowRef } from 'vue';
|
|
2
|
-
import { GameEngine, GameEventBus } from
|
|
2
|
+
import { GameEngine, GameEventBus } from '@skewedaspect/sage';
|
|
3
3
|
interface SageEngineContext {
|
|
4
4
|
engine: ShallowRef<GameEngine | null>;
|
|
5
5
|
eventBus: ComputedRef<GameEventBus | null>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
import { BehaviorClass } from '@skewedaspect/sage';
|
|
3
|
+
import { GameEntityView, ViewNarrowedTo } from "./useSageEntity.d.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Answers with a view for each entity carrying the tag, whatever its class, in the order the simulation
|
|
6
|
+
* holds them. A view stays the same object for as long as its entity is in the collection, so a `v-for`
|
|
7
|
+
* keyed by `id` reuses its components (design/ui.md#gameentityview).
|
|
8
|
+
*
|
|
9
|
+
* @param tag - The tag the collection re-reads on every refresh.
|
|
10
|
+
* @param behaviorClass - Narrows the collection to the entities holding it, typed to its state and operations.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSageEntities(tag: string): Ref<readonly GameEntityView[]>;
|
|
13
|
+
export declare function useSageEntities<Class extends BehaviorClass>(tag: string, behaviorClass: Class): Ref<readonly ViewNarrowedTo<Class>[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BehaviorClass, DeepReadonly, GameEntityType, OpsFor, OpsOf, StateFor, StateSliceOf } from '@skewedaspect/sage';
|
|
2
|
+
/** What a UI observes an entity through (design/ui.md#gameentityview). */
|
|
3
|
+
export interface GameEntityView<Type extends GameEntityType = GameEntityType> {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly present: boolean;
|
|
6
|
+
readonly state: DeepReadonly<StateFor<Type['behaviors']>>;
|
|
7
|
+
readonly ops: OpsFor<Type['behaviors']>;
|
|
8
|
+
hasBehavior<Class extends BehaviorClass>(behaviorClass: Class): this is ViewNarrowedTo<Class>;
|
|
9
|
+
}
|
|
10
|
+
/** A view typed to one behavior's slice of state and operations (design/ui.md#gameentityview). */
|
|
11
|
+
export type ViewNarrowedTo<Class extends BehaviorClass> = GameEntityView & {
|
|
12
|
+
readonly state: DeepReadonly<StateSliceOf<Class>>;
|
|
13
|
+
readonly ops: OpsOf<Class>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Answers with a view of one entity, resolved by class and ID against the simulation the engine currently
|
|
17
|
+
* holds. The view observes as it is created and refreshes once per rendered frame, pause included
|
|
18
|
+
* (design/ui.md#when-a-view-observes).
|
|
19
|
+
*
|
|
20
|
+
* @param entityClass - Types the view and checks it: another class leaves the view absent.
|
|
21
|
+
* @param id - The entity ID the view resolves on every refresh.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useSageEntity<Type extends GameEntityType>(entityClass: Type, id: string): GameEntityView<Type>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { GameEvent, LibraryEventPayloadMap } from '@skewedaspect/sage';
|
|
2
|
+
type EventHandler = (event: GameEvent<string, LibraryEventPayloadMap>) => void | Promise<void>;
|
|
3
|
+
export declare function useSageEvent(pattern: string | RegExp, handler: EventHandler): void;
|
|
4
|
+
export {};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { CooldownHandle } from
|
|
1
|
+
import { CooldownHandle } from '@skewedaspect/sage';
|
|
2
2
|
interface SageTimerComposable {
|
|
3
3
|
delay: (ms: number, callback: () => void) => () => void;
|
|
4
4
|
interval: (ms: number, callback: () => void) => () => void;
|
|
5
5
|
cooldown: (ms: number) => CooldownHandle;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Returns pause-aware timer functions (delay, interval, cooldown) from the game engine.
|
|
9
|
-
*
|
|
8
|
+
* Returns pause-aware timer functions (delay, interval, cooldown) from the game engine. Timers requested before the
|
|
9
|
+
* engine has booted are queued and started as soon as it does, so a component can set them up in `setup()`. All active
|
|
10
|
+
* timers are cancelled when the component unmounts.
|
|
10
11
|
*/
|
|
11
12
|
export declare function useSageTimer(): SageTimerComposable;
|
|
12
13
|
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BehaviorClass, GameEntity, GameEntityType, GameSimulationManager } from '@skewedaspect/sage';
|
|
2
|
+
type Bag = Record<string, unknown>;
|
|
3
|
+
/** Assigns what differs, adds what appeared, removes what vanished, and leaves everything else alone. */
|
|
4
|
+
export declare function syncBag(target: Bag, source: Readonly<Bag>): void;
|
|
5
|
+
/**
|
|
6
|
+
* Answers with the entity that ID names in the simulation the engine currently holds, when it is of the class
|
|
7
|
+
* asked for. A subclass is of that class (design/ui.md#gameentityview).
|
|
8
|
+
*
|
|
9
|
+
* @param simulation
|
|
10
|
+
* @param entityClass
|
|
11
|
+
* @param id
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveEntity(simulation: GameSimulationManager | null | undefined, entityClass: GameEntityType, id: string): GameEntity | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Answers with the entities carrying the tag, whatever their class, in the order the simulation holds them.
|
|
16
|
+
* A behavior narrows that to the ones holding it (design/ui.md#gameentityview).
|
|
17
|
+
*
|
|
18
|
+
* @param simulation
|
|
19
|
+
* @param tag
|
|
20
|
+
* @param behaviorClass
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveTagged(simulation: GameSimulationManager | null | undefined, tag: string, behaviorClass?: BehaviorClass): readonly GameEntity[];
|
|
23
|
+
/**
|
|
24
|
+
* Builds the narrowing guard a view exposes. It resolves the entity on every call, so it answers from the
|
|
25
|
+
* entity rather than from the view's copy of state, and a view whose entity has gone holds no behavior
|
|
26
|
+
* (design/ui.md#gameentityview).
|
|
27
|
+
*
|
|
28
|
+
* @param find
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildHasBehavior(find: () => GameEntity | undefined): (behaviorClass: BehaviorClass) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Builds the operation surface a view exposes. Every call resolves the entity again, so a view whose entity
|
|
33
|
+
* has gone throws rather than reaching the one it used to hold (design/ui.md#absence).
|
|
34
|
+
*
|
|
35
|
+
* @param id
|
|
36
|
+
* @param find
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildOps(id: string, find: () => GameEntity | undefined): Record<string, unknown>;
|
|
39
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export { default as SageCanvas } from './sageCanvas.vue';
|
|
2
|
-
export { useSageEngine } from "./useSageEngine.d.ts";
|
|
3
|
-
export { useSageEvent } from "./useSageEvent.d.ts";
|
|
4
|
-
export { useSageAction } from "./useSageAction.d.ts";
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
1
|
+
export { default as SageCanvas } from './components/sageCanvas.vue';
|
|
2
|
+
export { useSageEngine } from "./composables/useSageEngine.d.ts";
|
|
3
|
+
export { useSageEvent } from "./composables/useSageEvent.d.ts";
|
|
4
|
+
export { useSageAction } from "./composables/useSageAction.d.ts";
|
|
5
|
+
export { useSageTimer } from "./composables/useSageTimer.d.ts";
|
|
6
|
+
export { useSageEntity } from "./composables/useSageEntity.d.ts";
|
|
7
|
+
export type { GameEntityView, ViewNarrowedTo } from "./composables/useSageEntity.d.ts";
|
|
8
|
+
export { useSageEntities } from "./composables/useSageEntities.d.ts";
|
|
7
9
|
export { SAGE_ENGINE_KEY } from "./symbols.d.ts";
|
package/dist/sage-vue.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.sage-container[data-v-
|
|
1
|
+
.sage-container[data-v-978219c2]{width:100%;height:100%;position:relative}.sage-canvas[data-v-978219c2]{width:100%;height:100%;display:block}.sage-overlay[data-v-978219c2]{pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0}.sage-overlay[data-v-978219c2] *{pointer-events:auto}
|
|
2
2
|
/*$vite$:1*/
|
package/dist/sage-vue.js
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
import { computed as e, createElementBlock as t, createElementVNode as n, defineComponent as r, inject as i, onMounted as a, onUnmounted as o, openBlock as s, provide as c,
|
|
2
|
-
import { createGameEngine as
|
|
1
|
+
import { computed as e, createElementBlock as t, createElementVNode as n, defineComponent as r, inject as i, onMounted as a, onUnmounted as o, openBlock as s, provide as c, reactive as l, ref as u, renderSlot as d, shallowRef as f, useTemplateRef as p, watch as m } from "vue";
|
|
2
|
+
import { createGameEngine as h } from "@skewedaspect/sage";
|
|
3
3
|
//#region src/symbols.ts
|
|
4
|
-
var
|
|
4
|
+
var g = Symbol("sage-engine"), _ = { class: "sage-overlay" }, v = /*#__PURE__*/ ((e, t) => {
|
|
5
5
|
let n = e.__vccOpts || e;
|
|
6
6
|
for (let [e, r] of t) n[e] = r;
|
|
7
7
|
return n;
|
|
8
8
|
})(/* @__PURE__ */ r({
|
|
9
9
|
__name: "sageCanvas",
|
|
10
10
|
props: {
|
|
11
|
-
|
|
11
|
+
entityClasses: { default: () => ({}) },
|
|
12
12
|
options: { default: () => ({}) },
|
|
13
13
|
engine: {}
|
|
14
14
|
},
|
|
15
15
|
emits: ["engine-ready", "engine-error"],
|
|
16
16
|
setup(e, { emit: r }) {
|
|
17
|
-
let i = e,
|
|
18
|
-
return c(
|
|
17
|
+
let i = e, l = r, m = p("containerRef"), v = p("canvasRef"), y = f(null), b = u(!0), x = u(null), S = !1, C = null;
|
|
18
|
+
return c(g, y), a(async () => {
|
|
19
19
|
let e = !1;
|
|
20
20
|
if (o(() => {
|
|
21
21
|
e = !0;
|
|
22
22
|
}), v.value) try {
|
|
23
23
|
let t;
|
|
24
|
-
if (i.engine ? (t = i.engine, S = !0) : t = await
|
|
25
|
-
|
|
24
|
+
if (i.engine ? (t = i.engine, S = !0) : t = await h(v.value, i.entityClasses, i.options), e) {
|
|
25
|
+
S || (await t.stop(), t.renderEngine.dispose());
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
y.value = t, m.value && (C = new ResizeObserver(() => {
|
|
26
29
|
t.renderEngine.resize();
|
|
27
|
-
}), C.observe(
|
|
30
|
+
}), C.observe(m.value)), l("engine-ready", t), b.value = !1;
|
|
28
31
|
} catch (t) {
|
|
29
32
|
if (e) return;
|
|
30
|
-
x.value = t,
|
|
33
|
+
x.value = t, l("engine-error", t), b.value = !1;
|
|
31
34
|
}
|
|
32
35
|
}), o(async () => {
|
|
33
36
|
C &&= (C.disconnect(), null), !S && y.value && (await y.value.stop(), y.value.renderEngine.dispose());
|
|
34
37
|
}), (e, r) => (s(), t("div", {
|
|
35
38
|
ref_key: "containerRef",
|
|
36
|
-
ref:
|
|
39
|
+
ref: m,
|
|
37
40
|
class: "sage-container"
|
|
38
41
|
}, [n("canvas", {
|
|
39
42
|
ref_key: "canvasRef",
|
|
40
43
|
ref: v,
|
|
41
44
|
class: "sage-canvas"
|
|
42
|
-
}, null, 512), n("div",
|
|
45
|
+
}, null, 512), n("div", _, [d(e.$slots, "default", {
|
|
43
46
|
loading: b.value,
|
|
44
47
|
error: x.value,
|
|
45
48
|
engine: y.value
|
|
46
49
|
}, void 0, !0)])], 512));
|
|
47
50
|
}
|
|
48
|
-
}), [["__scopeId", "data-v-
|
|
51
|
+
}), [["__scopeId", "data-v-978219c2"]]);
|
|
49
52
|
//#endregion
|
|
50
|
-
//#region src/useSageEngine.ts
|
|
51
|
-
function
|
|
52
|
-
let t = i(
|
|
53
|
+
//#region src/composables/useSageEngine.ts
|
|
54
|
+
function y() {
|
|
55
|
+
let t = i(g);
|
|
53
56
|
if (!t) throw Error("useSageEngine() must be used inside a <SageCanvas> component.");
|
|
54
57
|
return {
|
|
55
58
|
engine: t,
|
|
@@ -57,76 +60,205 @@ function v() {
|
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
//#endregion
|
|
60
|
-
//#region src/useSageEvent.ts
|
|
61
|
-
function
|
|
62
|
-
let { engine: n } =
|
|
63
|
-
|
|
64
|
-
r &&= (r(), null), n && (r = n.eventBus.
|
|
65
|
-
}, {
|
|
63
|
+
//#region src/composables/useSageEvent.ts
|
|
64
|
+
function b(e, t) {
|
|
65
|
+
let { engine: n } = y(), r = null;
|
|
66
|
+
m(n, (n) => {
|
|
67
|
+
r &&= (r(), null), n && (r = n.eventBus.subscribe(e, t));
|
|
68
|
+
}, {
|
|
69
|
+
immediate: !0,
|
|
70
|
+
flush: "sync"
|
|
71
|
+
}), o(() => {
|
|
66
72
|
r &&= (r(), null);
|
|
67
73
|
});
|
|
68
74
|
}
|
|
69
75
|
//#endregion
|
|
70
|
-
//#region src/useSageAction.ts
|
|
71
|
-
function
|
|
72
|
-
let t =
|
|
73
|
-
return
|
|
76
|
+
//#region src/composables/useSageAction.ts
|
|
77
|
+
function x(e) {
|
|
78
|
+
let t = u(0);
|
|
79
|
+
return b(`action:${e}`, (e) => {
|
|
74
80
|
t.value = e.payload.value;
|
|
75
81
|
}), t;
|
|
76
82
|
}
|
|
77
83
|
//#endregion
|
|
78
|
-
//#region src/
|
|
79
|
-
function
|
|
80
|
-
let { engine:
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
get ready() {
|
|
92
|
-
return !1;
|
|
93
|
-
},
|
|
94
|
-
reset() {}
|
|
95
|
-
};
|
|
96
|
-
function w() {
|
|
97
|
-
let { engine: e } = v(), t = [];
|
|
98
|
-
o(() => {
|
|
84
|
+
//#region src/composables/useSageTimer.ts
|
|
85
|
+
function S() {
|
|
86
|
+
let { engine: e } = y(), t = [], n = /* @__PURE__ */ new Set();
|
|
87
|
+
m(e, (e) => {
|
|
88
|
+
if (!e) return;
|
|
89
|
+
let t = [...n];
|
|
90
|
+
n.clear();
|
|
91
|
+
for (let n of t) n(e);
|
|
92
|
+
}, {
|
|
93
|
+
immediate: !0,
|
|
94
|
+
flush: "sync"
|
|
95
|
+
}), o(() => {
|
|
96
|
+
n.clear();
|
|
99
97
|
for (let e of t) e();
|
|
100
98
|
t.length = 0;
|
|
101
99
|
});
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
i();
|
|
107
|
-
let e = t.indexOf(i);
|
|
108
|
-
e >= 0 && t.splice(e, 1);
|
|
109
|
-
};
|
|
100
|
+
function r(e) {
|
|
101
|
+
e();
|
|
102
|
+
let n = t.indexOf(e);
|
|
103
|
+
n >= 0 && t.splice(n, 1);
|
|
110
104
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
e >= 0 && t.splice(e, 1);
|
|
105
|
+
function i(i) {
|
|
106
|
+
let a = null, o = (e) => {
|
|
107
|
+
a = i(e), t.push(a);
|
|
108
|
+
};
|
|
109
|
+
return e.value ? o(e.value) : n.add(o), () => {
|
|
110
|
+
n.delete(o), a &&= (r(a), null);
|
|
118
111
|
};
|
|
119
112
|
}
|
|
120
|
-
function
|
|
121
|
-
return
|
|
113
|
+
function a(e, t) {
|
|
114
|
+
return i((n) => n.timer.delay(e, t));
|
|
115
|
+
}
|
|
116
|
+
function s(e, t) {
|
|
117
|
+
return i((n) => n.timer.interval(e, t));
|
|
118
|
+
}
|
|
119
|
+
function c(t) {
|
|
120
|
+
if (e.value) return e.value.timer.cooldown(t);
|
|
121
|
+
let r = null, i = !1;
|
|
122
|
+
return n.add((e) => {
|
|
123
|
+
r = e.timer.cooldown(t), i &&= (r.reset(), !1);
|
|
124
|
+
}), {
|
|
125
|
+
get ready() {
|
|
126
|
+
return r ? r.ready : !1;
|
|
127
|
+
},
|
|
128
|
+
reset() {
|
|
129
|
+
r ? r.reset() : i = !0;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
122
132
|
}
|
|
123
133
|
return {
|
|
124
|
-
delay:
|
|
125
|
-
interval:
|
|
126
|
-
cooldown:
|
|
134
|
+
delay: a,
|
|
135
|
+
interval: s,
|
|
136
|
+
cooldown: c
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/composables/viewSync.ts
|
|
141
|
+
function C(e) {
|
|
142
|
+
if (Array.isArray(e)) return e.map((e) => C(e));
|
|
143
|
+
if (typeof e == "object" && e) {
|
|
144
|
+
let t = {};
|
|
145
|
+
for (let [n, r] of Object.entries(e)) t[n] = C(r);
|
|
146
|
+
return t;
|
|
147
|
+
}
|
|
148
|
+
return e;
|
|
149
|
+
}
|
|
150
|
+
function w(e, t) {
|
|
151
|
+
if (Array.isArray(t)) {
|
|
152
|
+
if (!Array.isArray(e)) return C(t);
|
|
153
|
+
e.length !== t.length && (e.length = t.length);
|
|
154
|
+
for (let [n, r] of t.entries()) e[n] = w(e[n], r);
|
|
155
|
+
return e;
|
|
156
|
+
}
|
|
157
|
+
if (typeof t == "object" && t) {
|
|
158
|
+
if (typeof e != "object" || !e || Array.isArray(e)) return C(t);
|
|
159
|
+
let n = e, r = t;
|
|
160
|
+
for (let e of Object.keys(n)) e in r || Reflect.deleteProperty(n, e);
|
|
161
|
+
for (let [e, t] of Object.entries(r)) {
|
|
162
|
+
let r = w(n[e], t);
|
|
163
|
+
r !== n[e] && (n[e] = r);
|
|
164
|
+
}
|
|
165
|
+
return e;
|
|
166
|
+
}
|
|
167
|
+
return t;
|
|
168
|
+
}
|
|
169
|
+
function T(e, t) {
|
|
170
|
+
w(e, t);
|
|
171
|
+
}
|
|
172
|
+
function E(e, t, n) {
|
|
173
|
+
let r = e?.getEntity(n);
|
|
174
|
+
if (r !== void 0 && r instanceof t) return r;
|
|
175
|
+
}
|
|
176
|
+
function D(e, t, n) {
|
|
177
|
+
if (e == null) return [];
|
|
178
|
+
let r = e.getEntitiesByTag(t);
|
|
179
|
+
return n === void 0 ? r : r.filter((e) => e.hasBehavior(n));
|
|
180
|
+
}
|
|
181
|
+
function O(e) {
|
|
182
|
+
return (t) => e()?.hasBehavior(t) ?? !1;
|
|
183
|
+
}
|
|
184
|
+
function k(e, t) {
|
|
185
|
+
return new Proxy({}, { get(n, r) {
|
|
186
|
+
return (...n) => {
|
|
187
|
+
let i = t();
|
|
188
|
+
if (i === void 0) throw Error(`No entity "${e}" to call "${String(r)}" on.`);
|
|
189
|
+
return i.ops[String(r)](...n);
|
|
190
|
+
};
|
|
191
|
+
} });
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/composables/useSageEntity.ts
|
|
195
|
+
function A(e, t) {
|
|
196
|
+
let { engine: n } = y(), r = l({}), i = u(!1), a = () => E(n.value?.simulation, e, t), s = () => {
|
|
197
|
+
let e = a();
|
|
198
|
+
if (e === void 0) {
|
|
199
|
+
i.value = !1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
i.value = !0, T(r, e.state);
|
|
203
|
+
}, c = k(t, a), d = O(a), f = null;
|
|
204
|
+
return m(n, (e) => {
|
|
205
|
+
f?.(), f = null, s(), e !== null && (f = e.managers.gameManager.registerFrameCallback(s, { suspendedByPause: !1 }));
|
|
206
|
+
}, { immediate: !0 }), o(() => {
|
|
207
|
+
f?.(), f = null;
|
|
208
|
+
}), {
|
|
209
|
+
id: t,
|
|
210
|
+
get present() {
|
|
211
|
+
return i.value;
|
|
212
|
+
},
|
|
213
|
+
state: r,
|
|
214
|
+
ops: c,
|
|
215
|
+
hasBehavior(e) {
|
|
216
|
+
return d(e);
|
|
217
|
+
}
|
|
127
218
|
};
|
|
128
219
|
}
|
|
129
220
|
//#endregion
|
|
130
|
-
|
|
221
|
+
//#region src/composables/useSageEntities.ts
|
|
222
|
+
function j(e, t) {
|
|
223
|
+
if (e.length !== t.length) return !0;
|
|
224
|
+
for (let n = 0; n < t.length; n++) if (e[n] !== t[n]) return !0;
|
|
225
|
+
return !1;
|
|
226
|
+
}
|
|
227
|
+
function M(e, t) {
|
|
228
|
+
let { engine: n } = y(), r = f([]), i = /* @__PURE__ */ new Map(), a = (e) => n.value?.simulation?.getEntity(e), s = (e) => {
|
|
229
|
+
let t = i.get(e);
|
|
230
|
+
if (t !== void 0) return t;
|
|
231
|
+
let n = l({}), r = u(!0), o = () => a(e), s = k(e, o), c = O(o), d = {
|
|
232
|
+
state: n,
|
|
233
|
+
view: {
|
|
234
|
+
id: e,
|
|
235
|
+
get present() {
|
|
236
|
+
return r.value;
|
|
237
|
+
},
|
|
238
|
+
state: n,
|
|
239
|
+
ops: s,
|
|
240
|
+
hasBehavior(e) {
|
|
241
|
+
return c(e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
return i.set(e, d), d;
|
|
246
|
+
}, c = () => {
|
|
247
|
+
let a = D(n.value?.simulation, e, t), o = [], c = /* @__PURE__ */ new Set();
|
|
248
|
+
for (let e of a) {
|
|
249
|
+
let t = s(e.id);
|
|
250
|
+
T(t.state, e.state), o.push(t.view), c.add(e.id);
|
|
251
|
+
}
|
|
252
|
+
for (let e of [...i.keys()]) c.has(e) || i.delete(e);
|
|
253
|
+
j(r.value, o) && (r.value = o);
|
|
254
|
+
}, d = null;
|
|
255
|
+
return m(n, (e) => {
|
|
256
|
+
d?.(), d = null, c(), e !== null && (d = e.managers.gameManager.registerFrameCallback(c, { suspendedByPause: !1 }));
|
|
257
|
+
}, { immediate: !0 }), o(() => {
|
|
258
|
+
d?.(), d = null;
|
|
259
|
+
}), r;
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
262
|
+
export { g as SAGE_ENGINE_KEY, v as SageCanvas, x as useSageAction, y as useSageEngine, M as useSageEntities, A as useSageEntity, b as useSageEvent, S as useSageTimer };
|
|
131
263
|
|
|
132
264
|
//# sourceMappingURL=sage-vue.js.map
|
package/dist/sage-vue.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sage-vue.js","names":[],"sources":["../src/symbols.ts","../src/sageCanvas.vue","../src/sageCanvas.vue","../src/useSageEngine.ts","../src/useSageEvent.ts","../src/useSageAction.ts","../src/useSageState.ts","../src/useSageTimer.ts"],"sourcesContent":["//----------------------------------------------------------------------------------------------------------------------\n// Injection Key Symbols\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { InjectionKey, ShallowRef } from 'vue';\nimport type { GameEngine } from '@skewedaspect/sage';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport const SAGE_ENGINE_KEY : InjectionKey<ShallowRef<GameEngine | null>> = Symbol('sage-engine');\n\n//----------------------------------------------------------------------------------------------------------------------\n","<!----------------------------------------------------------------------------------------------------------------------\n - SageCanvas - Vue wrapper component for the SAGE game engine\n ----------------------------------------------------------------------------------------------------------------------->\n\n<template>\n <div ref=\"containerRef\" class=\"sage-container\">\n <canvas ref=\"canvasRef\" class=\"sage-canvas\"></canvas>\n <div class=\"sage-overlay\">\n <slot :loading=\"loading\" :error=\"error\" :engine=\"engineRef\"></slot>\n </div>\n </div>\n</template>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<style scoped>\n .sage-container {\n position: relative;\n width: 100%;\n height: 100%;\n }\n\n .sage-canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n .sage-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n }\n\n .sage-overlay :deep(*) {\n pointer-events: auto;\n }\n</style>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<script setup lang=\"ts\">\n import { ref, shallowRef, provide, onMounted, onUnmounted, useTemplateRef } from 'vue';\n import { createGameEngine } from '@skewedaspect/sage';\n import type { GameEngine, GameEntityDefinition, SageOptions } from '@skewedaspect/sage';\n import { SAGE_ENGINE_KEY } from './symbols.ts';\n\n //------------------------------------------------------------------------------------------------------------------\n // Props & Emits\n //------------------------------------------------------------------------------------------------------------------\n\n const props = withDefaults(defineProps<{\n entityDefinitions ?: GameEntityDefinition[];\n options ?: SageOptions;\n engine ?: GameEngine;\n }>(), {\n entityDefinitions: () => [],\n options: () => ({}),\n });\n\n const emit = defineEmits<{\n 'engine-ready': [engine : GameEngine];\n 'engine-error': [error : unknown];\n }>();\n\n //------------------------------------------------------------------------------------------------------------------\n // State\n //------------------------------------------------------------------------------------------------------------------\n\n const containerRef = useTemplateRef<HTMLDivElement>('containerRef');\n const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\n const engineRef = shallowRef<GameEngine | null>(null);\n const loading = ref(true);\n const error = ref<unknown>(null);\n let controlled = false;\n let resizeObserver : ResizeObserver | null = null;\n\n // Provide the engine ref synchronously so child composables can inject it\n provide(SAGE_ENGINE_KEY, engineRef);\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n onMounted(async() =>\n {\n let disposed = false;\n\n onUnmounted(() =>\n {\n disposed = true;\n });\n\n if(!canvasRef.value)\n {\n return;\n }\n\n try\n {\n let engine : GameEngine;\n\n if(props.engine)\n {\n engine = props.engine;\n controlled = true;\n }\n else\n {\n engine = await createGameEngine(canvasRef.value, props.entityDefinitions, props.options);\n }\n\n if(disposed) { return; }\n\n engineRef.value = engine;\n\n // Set up resize observer on the container\n if(containerRef.value)\n {\n resizeObserver = new ResizeObserver(() =>\n {\n engine.renderEngine.resize();\n });\n resizeObserver.observe(containerRef.value);\n }\n\n emit('engine-ready', engine);\n loading.value = false;\n }\n catch(err)\n {\n if(disposed) { return; }\n\n error.value = err;\n emit('engine-error', err);\n loading.value = false;\n }\n });\n\n onUnmounted(async() =>\n {\n if(resizeObserver)\n {\n resizeObserver.disconnect();\n resizeObserver = null;\n }\n\n if(!controlled && engineRef.value)\n {\n await engineRef.value.stop();\n engineRef.value.renderEngine.dispose();\n }\n });\n</script>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n","<!----------------------------------------------------------------------------------------------------------------------\n - SageCanvas - Vue wrapper component for the SAGE game engine\n ----------------------------------------------------------------------------------------------------------------------->\n\n<template>\n <div ref=\"containerRef\" class=\"sage-container\">\n <canvas ref=\"canvasRef\" class=\"sage-canvas\"></canvas>\n <div class=\"sage-overlay\">\n <slot :loading=\"loading\" :error=\"error\" :engine=\"engineRef\"></slot>\n </div>\n </div>\n</template>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<style scoped>\n .sage-container {\n position: relative;\n width: 100%;\n height: 100%;\n }\n\n .sage-canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n .sage-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n }\n\n .sage-overlay :deep(*) {\n pointer-events: auto;\n }\n</style>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<script setup lang=\"ts\">\n import { ref, shallowRef, provide, onMounted, onUnmounted, useTemplateRef } from 'vue';\n import { createGameEngine } from '@skewedaspect/sage';\n import type { GameEngine, GameEntityDefinition, SageOptions } from '@skewedaspect/sage';\n import { SAGE_ENGINE_KEY } from './symbols.ts';\n\n //------------------------------------------------------------------------------------------------------------------\n // Props & Emits\n //------------------------------------------------------------------------------------------------------------------\n\n const props = withDefaults(defineProps<{\n entityDefinitions ?: GameEntityDefinition[];\n options ?: SageOptions;\n engine ?: GameEngine;\n }>(), {\n entityDefinitions: () => [],\n options: () => ({}),\n });\n\n const emit = defineEmits<{\n 'engine-ready': [engine : GameEngine];\n 'engine-error': [error : unknown];\n }>();\n\n //------------------------------------------------------------------------------------------------------------------\n // State\n //------------------------------------------------------------------------------------------------------------------\n\n const containerRef = useTemplateRef<HTMLDivElement>('containerRef');\n const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\n const engineRef = shallowRef<GameEngine | null>(null);\n const loading = ref(true);\n const error = ref<unknown>(null);\n let controlled = false;\n let resizeObserver : ResizeObserver | null = null;\n\n // Provide the engine ref synchronously so child composables can inject it\n provide(SAGE_ENGINE_KEY, engineRef);\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n onMounted(async() =>\n {\n let disposed = false;\n\n onUnmounted(() =>\n {\n disposed = true;\n });\n\n if(!canvasRef.value)\n {\n return;\n }\n\n try\n {\n let engine : GameEngine;\n\n if(props.engine)\n {\n engine = props.engine;\n controlled = true;\n }\n else\n {\n engine = await createGameEngine(canvasRef.value, props.entityDefinitions, props.options);\n }\n\n if(disposed) { return; }\n\n engineRef.value = engine;\n\n // Set up resize observer on the container\n if(containerRef.value)\n {\n resizeObserver = new ResizeObserver(() =>\n {\n engine.renderEngine.resize();\n });\n resizeObserver.observe(containerRef.value);\n }\n\n emit('engine-ready', engine);\n loading.value = false;\n }\n catch(err)\n {\n if(disposed) { return; }\n\n error.value = err;\n emit('engine-error', err);\n loading.value = false;\n }\n });\n\n onUnmounted(async() =>\n {\n if(resizeObserver)\n {\n resizeObserver.disconnect();\n resizeObserver = null;\n }\n\n if(!controlled && engineRef.value)\n {\n await engineRef.value.stop();\n engineRef.value.renderEngine.dispose();\n }\n });\n</script>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEngine Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type ComputedRef, type ShallowRef, computed, inject } from 'vue';\nimport type { GameEngine, GameEventBus } from '@skewedaspect/sage';\nimport { SAGE_ENGINE_KEY } from './symbols.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface SageEngineContext\n{\n engine : ShallowRef<GameEngine | null>;\n eventBus : ComputedRef<GameEventBus | null>;\n}\n\nexport function useSageEngine() : SageEngineContext\n{\n const engine = inject(SAGE_ENGINE_KEY);\n if(!engine)\n {\n throw new Error('useSageEngine() must be used inside a <SageCanvas> component.');\n }\n\n return {\n engine,\n eventBus: computed(() => engine.value?.eventBus ?? null),\n };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEvent Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { onUnmounted, watch } from 'vue';\nimport type { GameEvent, Unsubscribe, WildcardEventCallback, WildcardPattern } from '@skewedaspect/sage';\nimport { useSageEngine } from './useSageEngine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype EventHandler = (event : GameEvent) => void | Promise<void>;\n\nexport function useSageEvent(pattern : string | RegExp, handler : EventHandler) : void\n{\n const { engine } = useSageEngine();\n let unsub : Unsubscribe | null = null;\n\n watch(engine, (eng) =>\n {\n if(unsub)\n {\n unsub();\n unsub = null;\n }\n\n if(eng)\n {\n unsub = eng.eventBus.subscribePattern(\n pattern as WildcardPattern | RegExp,\n handler as WildcardEventCallback\n );\n }\n }, { immediate: true });\n\n onUnmounted(() =>\n {\n if(unsub)\n {\n unsub();\n unsub = null;\n }\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageAction Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, ref } from 'vue';\nimport { useSageEvent } from './useSageEvent.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport function useSageAction(actionName : string) : Ref<boolean | number>\n{\n const value = ref<boolean | number>(0);\n\n useSageEvent(`action:${ actionName }`, (event) =>\n {\n value.value = (event.payload as { value : boolean | number }).value;\n });\n\n return value;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageState Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, ref } from 'vue';\nimport type { EntityStateChangedPayload } from '@skewedaspect/sage';\nimport { useSageEngine } from './useSageEngine.ts';\nimport { useSageEvent } from './useSageEvent.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Returns a reactive ref synced to an entity's state. If `key` is provided, tracks only that property; otherwise\n * tracks the entire state object.\n */\nexport function useSageState<T = unknown>(entityId : string, key ?: string) : Ref<T>\n{\n const { engine } = useSageEngine();\n\n // Read initial state from the entity (if it exists yet)\n const readState = () : T =>\n {\n const entity = engine.value?.managers.entityManager.getEntity(entityId);\n if(!entity)\n {\n return undefined as T;\n }\n\n if(key)\n {\n return (entity.state as Record<string, unknown>)[key] as T;\n }\n\n return entity.state as T;\n };\n\n const state = ref<T>(readState()) as Ref<T>;\n\n useSageEvent('entity:state-changed', (event) =>\n {\n const payload = event.payload as EntityStateChangedPayload;\n if(payload.entityId !== entityId)\n {\n return;\n }\n\n // Re-read from the entity to get the authoritative value\n state.value = readState();\n });\n\n return state;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageTimer Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { onUnmounted } from 'vue';\nimport type { CooldownHandle } from '@skewedaspect/sage';\nimport { useSageEngine } from './useSageEngine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface SageTimerComposable\n{\n delay : (ms : number, callback : () => void) => () => void;\n interval : (ms : number, callback : () => void) => () => void;\n cooldown : (ms : number) => CooldownHandle;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst NOOP = () : void => {};\n\nconst NOOP_COOLDOWN : CooldownHandle = {\n get ready() : boolean { return false; },\n reset() : void { /* no-op */ },\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Returns pause-aware timer functions (delay, interval, cooldown) from the game engine. All active timers are\n * automatically cancelled when the component unmounts.\n */\nexport function useSageTimer() : SageTimerComposable\n{\n const { engine } = useSageEngine();\n const cancellers : (() => void)[] = [];\n\n onUnmounted(() =>\n {\n for(const cancel of cancellers)\n {\n cancel();\n }\n\n cancellers.length = 0;\n });\n\n function delay(ms : number, callback : () => void) : () => void\n {\n if(!engine.value)\n {\n return NOOP;\n }\n\n const cancel = engine.value.timer.delay(ms, callback);\n cancellers.push(cancel);\n\n return () =>\n {\n cancel();\n const idx = cancellers.indexOf(cancel);\n if(idx >= 0)\n {\n cancellers.splice(idx, 1);\n }\n };\n }\n\n function interval(ms : number, callback : () => void) : () => void\n {\n if(!engine.value)\n {\n return NOOP;\n }\n\n const cancel = engine.value.timer.interval(ms, callback);\n cancellers.push(cancel);\n\n return () =>\n {\n cancel();\n const idx = cancellers.indexOf(cancel);\n if(idx >= 0)\n {\n cancellers.splice(idx, 1);\n }\n };\n }\n\n function cooldown(ms : number) : CooldownHandle\n {\n if(!engine.value)\n {\n return NOOP_COOLDOWN;\n }\n\n const handle = engine.value.timer.cooldown(ms);\n\n // There's no cancel for cooldowns, but we don't need to track them — they're passive pollable objects\n return handle;\n }\n\n return { delay, interval, cooldown };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sage-vue.js","names":[],"sources":["../src/symbols.ts","../src/components/sageCanvas.vue","../src/components/sageCanvas.vue","../src/composables/useSageEngine.ts","../src/composables/useSageEvent.ts","../src/composables/useSageAction.ts","../src/composables/useSageTimer.ts","../src/composables/viewSync.ts","../src/composables/useSageEntity.ts","../src/composables/useSageEntities.ts"],"sourcesContent":["//----------------------------------------------------------------------------------------------------------------------\n// Injection Key Symbols\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type { InjectionKey, ShallowRef } from 'vue';\nimport type { GameEngine } from '@skewedaspect/sage';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport const SAGE_ENGINE_KEY : InjectionKey<ShallowRef<GameEngine | null>> = Symbol('sage-engine');\n\n//----------------------------------------------------------------------------------------------------------------------\n","<!----------------------------------------------------------------------------------------------------------------------\n - SageCanvas - Vue wrapper component for the SAGE game engine\n ------------------------------------------------------------------------------------------------------------------->\n\n<template>\n <div ref=\"containerRef\" class=\"sage-container\">\n <canvas ref=\"canvasRef\" class=\"sage-canvas\" />\n <div class=\"sage-overlay\">\n <slot :loading=\"loading\" :error=\"error\" :engine=\"engineRef\" />\n </div>\n </div>\n</template>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<style scoped>\n .sage-container {\n position: relative;\n width: 100%;\n height: 100%;\n }\n\n .sage-canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n .sage-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n }\n\n .sage-overlay :deep(*) {\n pointer-events: auto;\n }\n</style>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<script setup lang=\"ts\">\n import { onMounted, onUnmounted, provide, ref, shallowRef, useTemplateRef } from 'vue';\n import { createGameEngine } from '@skewedaspect/sage';\n import type { GameEngine, GameEntityType, SageOptions } from '@skewedaspect/sage';\n import { SAGE_ENGINE_KEY } from '../symbols.ts';\n\n //------------------------------------------------------------------------------------------------------------------\n // Props & Emits\n //------------------------------------------------------------------------------------------------------------------\n\n const props = withDefaults(defineProps<{\n entityClasses ?: Readonly<Record<string, GameEntityType>>;\n options ?: SageOptions;\n engine ?: GameEngine;\n }>(), {\n entityClasses: () => ({}),\n options: () => ({}),\n });\n\n const emit = defineEmits<{\n 'engine-ready' : [engine : GameEngine];\n 'engine-error' : [error : unknown];\n }>();\n\n //------------------------------------------------------------------------------------------------------------------\n // State\n //------------------------------------------------------------------------------------------------------------------\n\n const containerRef = useTemplateRef<HTMLDivElement>('containerRef');\n const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\n const engineRef = shallowRef<GameEngine | null>(null);\n const loading = ref(true);\n const error = ref<unknown>(null);\n let controlled = false;\n let resizeObserver : ResizeObserver | null = null;\n\n // Provide the engine ref synchronously so child composables can inject it\n provide(SAGE_ENGINE_KEY, engineRef);\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n onMounted(async() =>\n {\n let disposed = false;\n\n onUnmounted(() =>\n {\n disposed = true;\n });\n\n if(!canvasRef.value)\n {\n return;\n }\n\n try\n {\n let engine : GameEngine;\n\n if(props.engine)\n {\n engine = props.engine;\n controlled = true;\n }\n else\n {\n engine = await createGameEngine(canvasRef.value, props.entityClasses, props.options);\n }\n\n // The component went away while the engine was still booting, so nothing else will ever hold a\n // reference to it. Tear it down here or it keeps rendering forever.\n if(disposed)\n {\n if(!controlled)\n {\n await engine.stop();\n engine.renderEngine.dispose();\n }\n\n return;\n }\n\n engineRef.value = engine;\n\n // Set up resize observer on the container\n if(containerRef.value)\n {\n resizeObserver = new ResizeObserver(() =>\n {\n engine.renderEngine.resize();\n });\n resizeObserver.observe(containerRef.value);\n }\n\n emit('engine-ready', engine);\n loading.value = false;\n }\n catch(err)\n {\n if(disposed) { return; }\n\n error.value = err;\n emit('engine-error', err);\n loading.value = false;\n }\n });\n\n onUnmounted(async() =>\n {\n if(resizeObserver)\n {\n resizeObserver.disconnect();\n resizeObserver = null;\n }\n\n if(!controlled && engineRef.value)\n {\n await engineRef.value.stop();\n engineRef.value.renderEngine.dispose();\n }\n });\n</script>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n","<!----------------------------------------------------------------------------------------------------------------------\n - SageCanvas - Vue wrapper component for the SAGE game engine\n ------------------------------------------------------------------------------------------------------------------->\n\n<template>\n <div ref=\"containerRef\" class=\"sage-container\">\n <canvas ref=\"canvasRef\" class=\"sage-canvas\" />\n <div class=\"sage-overlay\">\n <slot :loading=\"loading\" :error=\"error\" :engine=\"engineRef\" />\n </div>\n </div>\n</template>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<style scoped>\n .sage-container {\n position: relative;\n width: 100%;\n height: 100%;\n }\n\n .sage-canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n .sage-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n }\n\n .sage-overlay :deep(*) {\n pointer-events: auto;\n }\n</style>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n\n<script setup lang=\"ts\">\n import { onMounted, onUnmounted, provide, ref, shallowRef, useTemplateRef } from 'vue';\n import { createGameEngine } from '@skewedaspect/sage';\n import type { GameEngine, GameEntityType, SageOptions } from '@skewedaspect/sage';\n import { SAGE_ENGINE_KEY } from '../symbols.ts';\n\n //------------------------------------------------------------------------------------------------------------------\n // Props & Emits\n //------------------------------------------------------------------------------------------------------------------\n\n const props = withDefaults(defineProps<{\n entityClasses ?: Readonly<Record<string, GameEntityType>>;\n options ?: SageOptions;\n engine ?: GameEngine;\n }>(), {\n entityClasses: () => ({}),\n options: () => ({}),\n });\n\n const emit = defineEmits<{\n 'engine-ready' : [engine : GameEngine];\n 'engine-error' : [error : unknown];\n }>();\n\n //------------------------------------------------------------------------------------------------------------------\n // State\n //------------------------------------------------------------------------------------------------------------------\n\n const containerRef = useTemplateRef<HTMLDivElement>('containerRef');\n const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef');\n const engineRef = shallowRef<GameEngine | null>(null);\n const loading = ref(true);\n const error = ref<unknown>(null);\n let controlled = false;\n let resizeObserver : ResizeObserver | null = null;\n\n // Provide the engine ref synchronously so child composables can inject it\n provide(SAGE_ENGINE_KEY, engineRef);\n\n //------------------------------------------------------------------------------------------------------------------\n // Lifecycle\n //------------------------------------------------------------------------------------------------------------------\n\n onMounted(async() =>\n {\n let disposed = false;\n\n onUnmounted(() =>\n {\n disposed = true;\n });\n\n if(!canvasRef.value)\n {\n return;\n }\n\n try\n {\n let engine : GameEngine;\n\n if(props.engine)\n {\n engine = props.engine;\n controlled = true;\n }\n else\n {\n engine = await createGameEngine(canvasRef.value, props.entityClasses, props.options);\n }\n\n // The component went away while the engine was still booting, so nothing else will ever hold a\n // reference to it. Tear it down here or it keeps rendering forever.\n if(disposed)\n {\n if(!controlled)\n {\n await engine.stop();\n engine.renderEngine.dispose();\n }\n\n return;\n }\n\n engineRef.value = engine;\n\n // Set up resize observer on the container\n if(containerRef.value)\n {\n resizeObserver = new ResizeObserver(() =>\n {\n engine.renderEngine.resize();\n });\n resizeObserver.observe(containerRef.value);\n }\n\n emit('engine-ready', engine);\n loading.value = false;\n }\n catch(err)\n {\n if(disposed) { return; }\n\n error.value = err;\n emit('engine-error', err);\n loading.value = false;\n }\n });\n\n onUnmounted(async() =>\n {\n if(resizeObserver)\n {\n resizeObserver.disconnect();\n resizeObserver = null;\n }\n\n if(!controlled && engineRef.value)\n {\n await engineRef.value.stop();\n engineRef.value.renderEngine.dispose();\n }\n });\n</script>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEngine Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type ComputedRef, type ShallowRef, computed, inject } from 'vue';\nimport type { GameEngine, GameEventBus } from '@skewedaspect/sage';\nimport { SAGE_ENGINE_KEY } from '../symbols.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface SageEngineContext\n{\n engine : ShallowRef<GameEngine | null>;\n eventBus : ComputedRef<GameEventBus | null>;\n}\n\nexport function useSageEngine() : SageEngineContext\n{\n const engine = inject(SAGE_ENGINE_KEY);\n if(!engine)\n {\n throw new Error('useSageEngine() must be used inside a <SageCanvas> component.');\n }\n\n return {\n engine,\n eventBus: computed(() => engine.value?.eventBus ?? null),\n };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEvent Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { onUnmounted, watch } from 'vue';\nimport type { GameEvent, LibraryEventPayloadMap, Unsubscribe } from '@skewedaspect/sage';\nimport { useSageEngine } from './useSageEngine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype EventHandler = (event : GameEvent<string, LibraryEventPayloadMap>) => void | Promise<void>;\n\nexport function useSageEvent(pattern : string | RegExp, handler : EventHandler) : void\n{\n const { engine } = useSageEngine();\n let unsub : Unsubscribe | null = null;\n\n // Sync flush, not the default 'pre': SageCanvas assigns the engine and emits engine-ready in one turn, so a\n // deferred watcher would miss anything published in that window.\n watch(engine, (eng) =>\n {\n if(unsub)\n {\n unsub();\n unsub = null;\n }\n\n if(eng)\n {\n // subscribe() sends an exact type to the bus's direct map and anything wildcard-ish to the pattern list.\n unsub = eng.eventBus.subscribe(pattern, handler);\n }\n }, { immediate: true, flush: 'sync' });\n\n onUnmounted(() =>\n {\n if(unsub)\n {\n unsub();\n unsub = null;\n }\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageAction Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, ref } from 'vue';\nimport { useSageEvent } from './useSageEvent.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\nexport function useSageAction(actionName : string) : Ref<boolean | number>\n{\n const value = ref<boolean | number>(0);\n\n useSageEvent(`action:${ actionName }`, (event) =>\n {\n value.value = (event.payload as { value : boolean | number }).value;\n });\n\n return value;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageTimer Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { onUnmounted, watch } from 'vue';\nimport type { CooldownHandle, GameEngine } from '@skewedaspect/sage';\nimport { useSageEngine } from './useSageEngine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface SageTimerComposable\n{\n delay : (ms : number, callback : () => void) => () => void;\n interval : (ms : number, callback : () => void) => () => void;\n cooldown : (ms : number) => CooldownHandle;\n}\n\n/** A registration waiting for the engine to boot. */\ntype PendingTimer = (engine : GameEngine) => void;\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Returns pause-aware timer functions (delay, interval, cooldown) from the game engine. Timers requested before the\n * engine has booted are queued and started as soon as it does, so a component can set them up in `setup()`. All active\n * timers are cancelled when the component unmounts.\n */\nexport function useSageTimer() : SageTimerComposable\n{\n const { engine } = useSageEngine();\n const cancellers : (() => void)[] = [];\n const pending = new Set<PendingTimer>();\n\n // Sync flush so queued timers start the moment the engine lands, rather than a scheduler tick later.\n watch(engine, (eng) =>\n {\n if(!eng)\n {\n return;\n }\n\n const queued = [ ...pending ];\n pending.clear();\n\n for(const start of queued)\n {\n start(eng);\n }\n }, { immediate: true, flush: 'sync' });\n\n onUnmounted(() =>\n {\n pending.clear();\n\n for(const cancel of cancellers)\n {\n cancel();\n }\n\n cancellers.length = 0;\n });\n\n function release(cancel : () => void) : void\n {\n cancel();\n\n const idx = cancellers.indexOf(cancel);\n if(idx >= 0)\n {\n cancellers.splice(idx, 1);\n }\n }\n\n /**\n * Runs `start` against the engine now, or queues it until the engine boots. The returned canceller covers both\n * cases: called while still queued, it drops the registration so it never starts at all.\n */\n function schedule(start : (eng : GameEngine) => () => void) : () => void\n {\n let cancel : (() => void) | null = null;\n\n const begin = (eng : GameEngine) : void =>\n {\n cancel = start(eng);\n cancellers.push(cancel);\n };\n\n if(engine.value)\n {\n begin(engine.value);\n }\n else\n {\n pending.add(begin);\n }\n\n return () =>\n {\n pending.delete(begin);\n\n if(cancel)\n {\n release(cancel);\n cancel = null;\n }\n };\n }\n\n function delay(ms : number, callback : () => void) : () => void\n {\n return schedule((eng) => eng.timer.delay(ms, callback));\n }\n\n function interval(ms : number, callback : () => void) : () => void\n {\n return schedule((eng) => eng.timer.interval(ms, callback));\n }\n\n /**\n * Cooldowns are passive pollable objects with nothing to cancel, so they aren't tracked for unmount. Before the\n * engine boots the caller gets a stand-in that forwards to the real handle once there is one; a `reset()` landing\n * in the gap is replayed against it.\n */\n function cooldown(ms : number) : CooldownHandle\n {\n if(engine.value)\n {\n return engine.value.timer.cooldown(ms);\n }\n\n let handle : CooldownHandle | null = null;\n let resetWhileWaiting = false;\n\n pending.add((eng) =>\n {\n handle = eng.timer.cooldown(ms);\n\n if(resetWhileWaiting)\n {\n handle.reset();\n resetWhileWaiting = false;\n }\n });\n\n return {\n get ready() : boolean\n {\n return handle ? handle.ready : false;\n },\n reset() : void\n {\n if(handle)\n {\n handle.reset();\n }\n else\n {\n resetWhileWaiting = true;\n }\n },\n };\n }\n\n return { delay, interval, cooldown };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// View Synchronisation\n//\n// The walk a view refreshes by, and the resolution and operation surface both composables share\n// (design/ui.md#how-a-view-notices-a-change).\n//----------------------------------------------------------------------------------------------------------------------\n\nimport type {\n BehaviorClass, GameEntity, GameEntityType, GameSimulationManager,\n} from '@skewedaspect/sage';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype Bag = Record<string, unknown>;\n\n/** State is finite JSON-compatible data, so a copy needs nothing exotic (design/entities.md#state). */\nfunction copyValue(value : unknown) : unknown\n{\n if(Array.isArray(value))\n {\n return value.map((item) => copyValue(item));\n }\n\n if(value !== null && typeof value === 'object')\n {\n const copy : Bag = {};\n\n for(const [ key, field ] of Object.entries(value as Bag))\n {\n copy[key] = copyValue(field);\n }\n\n return copy;\n }\n\n return value;\n}\n\n/**\n * Answers with what the view should hold for one value: the target itself once it has been brought into\n * line, or a copy when the target cannot be reused. A view never holds an object entity state also holds, so\n * a later change inside that object is one the walk can still see.\n */\nfunction syncValue(current : unknown, next : unknown) : unknown\n{\n if(Array.isArray(next))\n {\n if(!Array.isArray(current))\n {\n return copyValue(next);\n }\n\n if(current.length !== next.length)\n {\n current.length = next.length;\n }\n\n for(const [ index, item ] of next.entries())\n {\n current[index] = syncValue(current[index], item);\n }\n\n return current;\n }\n\n if(next !== null && typeof next === 'object')\n {\n if(current === null || typeof current !== 'object' || Array.isArray(current))\n {\n return copyValue(next);\n }\n\n const target = current as Bag;\n const source = next as Bag;\n\n for(const key of Object.keys(target))\n {\n if(!(key in source))\n {\n Reflect.deleteProperty(target, key);\n }\n }\n\n for(const [ key, field ] of Object.entries(source))\n {\n const settled = syncValue(target[key], field);\n\n if(settled !== target[key])\n {\n target[key] = settled;\n }\n }\n\n return current;\n }\n\n return next;\n}\n\n/** Assigns what differs, adds what appeared, removes what vanished, and leaves everything else alone. */\nexport function syncBag(target : Bag, source : Readonly<Bag>) : void\n{\n syncValue(target, source);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype EntityConstructor = abstract new (...args : never[]) => GameEntity;\n\n/**\n * Answers with the entity that ID names in the simulation the engine currently holds, when it is of the class\n * asked for. A subclass is of that class (design/ui.md#gameentityview).\n *\n * @param simulation\n * @param entityClass\n * @param id\n */\nexport function resolveEntity(\n simulation : GameSimulationManager | null | undefined,\n entityClass : GameEntityType,\n id : string\n) : GameEntity | undefined\n{\n const entity = simulation?.getEntity(id);\n\n if(entity === undefined)\n {\n return undefined;\n }\n\n if(!(entity instanceof (entityClass as unknown as EntityConstructor)))\n {\n return undefined;\n }\n\n return entity;\n}\n\n/**\n * Answers with the entities carrying the tag, whatever their class, in the order the simulation holds them.\n * A behavior narrows that to the ones holding it (design/ui.md#gameentityview).\n *\n * @param simulation\n * @param tag\n * @param behaviorClass\n */\nexport function resolveTagged(\n simulation : GameSimulationManager | null | undefined,\n tag : string,\n behaviorClass ?: BehaviorClass\n) : readonly GameEntity[]\n{\n if(simulation === null || simulation === undefined)\n {\n return [];\n }\n\n const tagged = simulation.getEntitiesByTag(tag);\n\n if(behaviorClass === undefined)\n {\n return tagged;\n }\n\n return tagged.filter((entity) => entity.hasBehavior(behaviorClass));\n}\n\n/**\n * Builds the narrowing guard a view exposes. It resolves the entity on every call, so it answers from the\n * entity rather than from the view's copy of state, and a view whose entity has gone holds no behavior\n * (design/ui.md#gameentityview).\n *\n * @param find\n */\nexport function buildHasBehavior(\n find : () => GameEntity | undefined\n) : (behaviorClass : BehaviorClass) => boolean\n{\n return (behaviorClass) =>\n {\n return find()?.hasBehavior(behaviorClass) ?? false;\n };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Builds the operation surface a view exposes. Every call resolves the entity again, so a view whose entity\n * has gone throws rather than reaching the one it used to hold (design/ui.md#absence).\n *\n * @param id\n * @param find\n */\nexport function buildOps(id : string, find : () => GameEntity | undefined) : Record<string, unknown>\n{\n return new Proxy({}, {\n get(_target : object, name : string | symbol) : unknown\n {\n return (...args : unknown[]) : unknown =>\n {\n const entity = find();\n\n if(entity === undefined)\n {\n throw new Error(`No entity \"${ id }\" to call \"${ String(name) }\" on.`);\n }\n\n const ops = entity.ops as Record<string, (...called : unknown[]) => unknown>;\n\n return ops[String(name)](...args);\n };\n },\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEntity Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { onUnmounted, reactive, ref, watch } from 'vue';\nimport type {\n BehaviorClass, DeepReadonly, GameEntityType, OpsFor, OpsOf, StateFor, StateSliceOf,\n} from '@skewedaspect/sage';\n\nimport { useSageEngine } from './useSageEngine.ts';\nimport { buildHasBehavior, buildOps, resolveEntity, syncBag } from './viewSync.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/** What a UI observes an entity through (design/ui.md#gameentityview). */\nexport interface GameEntityView<Type extends GameEntityType = GameEntityType>\n{\n readonly id : string;\n readonly present : boolean;\n readonly state : DeepReadonly<StateFor<Type['behaviors']>>;\n readonly ops : OpsFor<Type['behaviors']>;\n\n hasBehavior<Class extends BehaviorClass>(behaviorClass : Class) : this is ViewNarrowedTo<Class>;\n}\n\n/** A view typed to one behavior's slice of state and operations (design/ui.md#gameentityview). */\nexport type ViewNarrowedTo<Class extends BehaviorClass> = GameEntityView & {\n readonly state : DeepReadonly<StateSliceOf<Class>>;\n readonly ops : OpsOf<Class>;\n};\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Answers with a view of one entity, resolved by class and ID against the simulation the engine currently\n * holds. The view observes as it is created and refreshes once per rendered frame, pause included\n * (design/ui.md#when-a-view-observes).\n *\n * @param entityClass - Types the view and checks it: another class leaves the view absent.\n * @param id - The entity ID the view resolves on every refresh.\n */\nexport function useSageEntity<Type extends GameEntityType>(\n entityClass : Type,\n id : string\n) : GameEntityView<Type>\n{\n const { engine } = useSageEngine();\n\n const state = reactive({}) as Record<string, unknown>;\n const present = ref(false);\n\n const find = () : ReturnType<typeof resolveEntity> =>\n {\n return resolveEntity(engine.value?.simulation, entityClass, id);\n };\n\n const refresh = () : void =>\n {\n const entity = find();\n\n if(entity === undefined)\n {\n // The values already read stay, so a template reads a field without guarding each one\n // (design/ui.md#absence).\n present.value = false;\n return;\n }\n\n present.value = true;\n syncBag(state, entity.state as Record<string, unknown>);\n };\n\n const ops = buildOps(id, find);\n const hasBehavior = buildHasBehavior(find);\n\n let unregister : (() => void) | null = null;\n\n watch(engine, (current) =>\n {\n unregister?.();\n unregister = null;\n\n refresh();\n\n if(current !== null)\n {\n unregister = current.managers.gameManager.registerFrameCallback(refresh, { suspendedByPause: false });\n }\n }, { immediate: true });\n\n onUnmounted(() =>\n {\n unregister?.();\n unregister = null;\n });\n\n return {\n id,\n get present() : boolean { return present.value; },\n state: state as DeepReadonly<StateFor<Type['behaviors']>>,\n ops: ops as OpsFor<Type['behaviors']>,\n hasBehavior<Class extends BehaviorClass>(behaviorClass : Class) : this is ViewNarrowedTo<Class>\n {\n return hasBehavior(behaviorClass);\n },\n };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEntities Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, onUnmounted, reactive, ref, shallowRef, watch } from 'vue';\nimport type {\n BehaviorClass, DeepReadonly, GameEntity, GameEntityType, OpsFor, StateFor,\n} from '@skewedaspect/sage';\n\nimport { useSageEngine } from './useSageEngine.ts';\nimport type { GameEntityView, ViewNarrowedTo } from './useSageEntity.ts';\nimport { buildHasBehavior, buildOps, resolveTagged, syncBag } from './viewSync.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface Held\n{\n view : GameEntityView;\n state : Record<string, unknown>;\n}\n\n// The array is replaced only when the collection's membership or its order changed, so a UI reading the\n// collection re-renders for a join, a departure, or a reorder and not once a frame\n// (design/ui.md#how-a-view-notices-a-change).\nfunction membershipChanged(current : readonly GameEntityView[], next : readonly GameEntityView[]) : boolean\n{\n if(current.length !== next.length)\n {\n return true;\n }\n\n for(let index = 0; index < next.length; index++)\n {\n if(current[index] !== next[index])\n {\n return true;\n }\n }\n\n return false;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Answers with a view for each entity carrying the tag, whatever its class, in the order the simulation\n * holds them. A view stays the same object for as long as its entity is in the collection, so a `v-for`\n * keyed by `id` reuses its components (design/ui.md#gameentityview).\n *\n * @param tag - The tag the collection re-reads on every refresh.\n * @param behaviorClass - Narrows the collection to the entities holding it, typed to its state and operations.\n */\nexport function useSageEntities(tag : string) : Ref<readonly GameEntityView[]>;\nexport function useSageEntities<Class extends BehaviorClass>(\n tag : string,\n behaviorClass : Class\n) : Ref<readonly ViewNarrowedTo<Class>[]>;\nexport function useSageEntities(\n tag : string,\n behaviorClass ?: BehaviorClass\n) : Ref<readonly GameEntityView[]>\n{\n const { engine } = useSageEngine();\n\n const views = shallowRef<readonly GameEntityView[]>([]);\n const held = new Map<string, Held>();\n\n const findEntity = (id : string) : GameEntity | undefined =>\n {\n return engine.value?.simulation?.getEntity(id);\n };\n\n const viewFor = (id : string) : Held =>\n {\n const existing = held.get(id);\n\n if(existing !== undefined)\n {\n return existing;\n }\n\n const state = reactive({}) as Record<string, unknown>;\n const present = ref(true);\n const find = () : GameEntity | undefined => findEntity(id);\n const ops = buildOps(id, find);\n const hasBehavior = buildHasBehavior(find);\n\n const record : Held = {\n state,\n view: {\n id,\n get present() : boolean { return present.value; },\n state: state as DeepReadonly<StateFor<GameEntityType['behaviors']>>,\n ops: ops as OpsFor<GameEntityType['behaviors']>,\n hasBehavior<Class extends BehaviorClass>(named : Class) : this is ViewNarrowedTo<Class>\n {\n return hasBehavior(named);\n },\n },\n };\n\n held.set(id, record);\n\n return record;\n };\n\n const refresh = () : void =>\n {\n const matched = resolveTagged(engine.value?.simulation, tag, behaviorClass);\n const next : GameEntityView[] = [];\n const seen = new Set<string>();\n\n for(const entity of matched)\n {\n const record = viewFor(entity.id);\n\n syncBag(record.state, entity.state as Record<string, unknown>);\n next.push(record.view);\n seen.add(entity.id);\n }\n\n for(const id of [ ...held.keys() ])\n {\n if(!seen.has(id))\n {\n held.delete(id);\n }\n }\n\n if(membershipChanged(views.value, next))\n {\n views.value = next;\n }\n };\n\n let unregister : (() => void) | null = null;\n\n watch(engine, (current) =>\n {\n unregister?.();\n unregister = null;\n\n refresh();\n\n if(current !== null)\n {\n unregister = current.managers.gameManager.registerFrameCallback(refresh, { suspendedByPause: false });\n }\n }, { immediate: true });\n\n onUnmounted(() =>\n {\n unregister?.();\n unregister = null;\n });\n\n return views;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n"],"mappings":";;;AASA,IAAa,IAAgE,OAAO,aAAa;;;;;;;;;;;;;EC6C7F,IAAM,IAAQ,GASR,IAAO,GASP,IAAe,EAA+B,cAAc,GAC5D,IAAY,EAAkC,WAAW,GACzD,IAAY,EAA8B,IAAI,GAC9C,IAAU,EAAI,EAAI,GAClB,IAAQ,EAAa,IAAI,GAC3B,IAAa,IACb,IAAyC;SAG7C,EAAQ,GAAiB,CAAS,GAMlC,EAAU,YACV;GACI,IAAI,IAAW;GAEf,YACA;IACI,IAAW;GACf,CAAC,GAEG,EAAU,OAKd,IACA;IACI,IAAI;IAcJ,IAZG,EAAM,UAEL,IAAS,EAAM,QACf,IAAa,MAIb,IAAS,MAAM,EAAiB,EAAU,OAAO,EAAM,eAAe,EAAM,OAAO,GAKpF,GACH;KACI,AAAI,MAEA,MAAM,EAAO,KAAK,GAClB,EAAO,aAAa,QAAQ;KAGhC;IACJ;IAeA,AAbA,EAAU,QAAQ,GAGf,EAAa,UAEZ,IAAiB,IAAI,qBACrB;KACI,EAAO,aAAa,OAAO;IAC/B,CAAC,GACD,EAAe,QAAQ,EAAa,KAAK,IAG7C,EAAK,gBAAgB,CAAM,GAC3B,EAAQ,QAAQ;GACpB,SACM,GACN;IACI,IAAG,GAAY;IAIf,AAFA,EAAM,QAAQ,GACd,EAAK,gBAAgB,CAAG,GACxB,EAAQ,QAAQ;GACpB;EACJ,CAAC,GAED,EAAY,YACZ;GAOI,AANA,AAGI,OADA,EAAe,WAAW,GACT,OAGlB,CAAC,KAAc,EAAU,UAExB,MAAM,EAAU,MAAM,KAAK,GAC3B,EAAU,MAAM,aAAa,QAAQ;EAE7C,CAAC,cAjKD,EAAA,GAAA,EAKM,OAAA;GALG,SAAA;GAAJ,KAAI;GAAe,OAAM;EAC1B,GAAA,CAAA,EAA8C,UAAA;GAAlC,SAAA;GAAJ,KAAI;GAAY,OAAM;EAC9B,GAAA,MAAA,GAAA,GAAA,EAEM,OAFN,GAEM,CADF,EAA8D,EAAA,QAAA,WAAA;GAAvD,SAAS,EAAA;GAAU,OAAO,EAAA;GAAQ,QAAQ,EAAA;;;;;;AEQ7D,SAAgB,IAChB;CACI,IAAM,IAAS,EAAO,CAAe;CACrC,IAAG,CAAC,GAEA,MAAU,MAAM,+DAA+D;CAGnF,OAAO;EACH;EACA,UAAU,QAAe,EAAO,OAAO,YAAY,IAAI;CAC3D;AACJ;;;AChBA,SAAgB,EAAa,GAA2B,GACxD;CACI,IAAM,EAAE,cAAW,EAAc,GAC7B,IAA6B;CAmBjC,AAfA,EAAM,IAAS,MACf;EAOI,AANA,AAGI,OADA,EAAM,GACE,OAGT,MAGC,IAAQ,EAAI,SAAS,UAAU,GAAS,CAAO;CAEvD,GAAG;EAAE,WAAW;EAAM,OAAO;CAAO,CAAC,GAErC,QACA;EACI,AAGI,OADA,EAAM,GACE;CAEhB,CAAC;AACL;;;ACjCA,SAAgB,EAAc,GAC9B;CACI,IAAM,IAAQ,EAAsB,CAAC;CAOrC,OALA,EAAa,UAAW,MAAgB,MACxC;EACI,EAAM,QAAS,EAAM,QAAyC;CAClE,CAAC,GAEM;AACX;;;ACQA,SAAgB,IAChB;CACI,IAAM,EAAE,cAAW,EAAc,GAC3B,IAA8B,CAAC,GAC/B,oBAAU,IAAI,IAAkB;CAmBtC,AAhBA,EAAM,IAAS,MACf;EACI,IAAG,CAAC,GAEA;EAGJ,IAAM,IAAS,CAAE,GAAG,CAAQ;EAC5B,EAAQ,MAAM;EAEd,KAAI,IAAM,KAAS,GAEf,EAAM,CAAG;CAEjB,GAAG;EAAE,WAAW;EAAM,OAAO;CAAO,CAAC,GAErC,QACA;EACI,EAAQ,MAAM;EAEd,KAAI,IAAM,KAAU,GAEhB,EAAO;EAGX,EAAW,SAAS;CACxB,CAAC;CAED,SAAS,EAAQ,GACjB;EACI,EAAO;EAEP,IAAM,IAAM,EAAW,QAAQ,CAAM;EACrC,AAAG,KAAO,KAEN,EAAW,OAAO,GAAK,CAAC;CAEhC;CAMA,SAAS,EAAS,GAClB;EACI,IAAI,IAA+B,MAE7B,KAAS,MACf;GAEI,AADA,IAAS,EAAM,CAAG,GAClB,EAAW,KAAK,CAAM;EAC1B;EAWA,OATG,EAAO,QAEN,EAAM,EAAO,KAAK,IAIlB,EAAQ,IAAI,CAAK,SAIrB;GAGI,AAFA,EAAQ,OAAO,CAAK,GAEpB,AAGI,OADA,EAAQ,CAAM,GACL;EAEjB;CACJ;CAEA,SAAS,EAAM,GAAa,GAC5B;EACI,OAAO,GAAU,MAAQ,EAAI,MAAM,MAAM,GAAI,CAAQ,CAAC;CAC1D;CAEA,SAAS,EAAS,GAAa,GAC/B;EACI,OAAO,GAAU,MAAQ,EAAI,MAAM,SAAS,GAAI,CAAQ,CAAC;CAC7D;CAOA,SAAS,EAAS,GAClB;EACI,IAAG,EAAO,OAEN,OAAO,EAAO,MAAM,MAAM,SAAS,CAAE;EAGzC,IAAI,IAAiC,MACjC,IAAoB;EAaxB,OAXA,EAAQ,KAAK,MACb;GAGI,AAFA,IAAS,EAAI,MAAM,SAAS,CAAE,GAE9B,AAGI,OADA,EAAO,MAAM,GACO;EAE5B,CAAC,GAEM;GACH,IAAI,QACJ;IACI,OAAO,IAAS,EAAO,QAAQ;GACnC;GACA,QACA;IACI,AAAG,IAEC,EAAO,MAAM,IAIb,IAAoB;GAE5B;EACJ;CACJ;CAEA,OAAO;EAAE;EAAO;EAAU;CAAS;AACvC;;;ACpJA,SAAS,EAAU,GACnB;CACI,IAAG,MAAM,QAAQ,CAAK,GAElB,OAAO,EAAM,KAAK,MAAS,EAAU,CAAI,CAAC;CAG9C,IAAqB,OAAO,KAAU,YAAnC,GACH;EACI,IAAM,IAAa,CAAC;EAEpB,KAAI,IAAM,CAAE,GAAK,MAAW,OAAO,QAAQ,CAAY,GAEnD,EAAK,KAAO,EAAU,CAAK;EAG/B,OAAO;CACX;CAEA,OAAO;AACX;AAOA,SAAS,EAAU,GAAmB,GACtC;CACI,IAAG,MAAM,QAAQ,CAAI,GACrB;EACI,IAAG,CAAC,MAAM,QAAQ,CAAO,GAErB,OAAO,EAAU,CAAI;EAGzB,AAAG,EAAQ,WAAW,EAAK,WAEvB,EAAQ,SAAS,EAAK;EAG1B,KAAI,IAAM,CAAE,GAAO,MAAU,EAAK,QAAQ,GAEtC,EAAQ,KAAS,EAAU,EAAQ,IAAQ,CAAI;EAGnD,OAAO;CACX;CAEA,IAAoB,OAAO,KAAS,YAAjC,GACH;EACI,IAAuB,OAAO,KAAY,aAAvC,KAAmD,MAAM,QAAQ,CAAO,GAEvE,OAAO,EAAU,CAAI;EAGzB,IAAM,IAAS,GACT,IAAS;EAEf,KAAI,IAAM,KAAO,OAAO,KAAK,CAAM,GAE/B,AAAK,KAAO,KAER,QAAQ,eAAe,GAAQ,CAAG;EAI1C,KAAI,IAAM,CAAE,GAAK,MAAW,OAAO,QAAQ,CAAM,GACjD;GACI,IAAM,IAAU,EAAU,EAAO,IAAM,CAAK;GAE5C,AAAG,MAAY,EAAO,OAElB,EAAO,KAAO;EAEtB;EAEA,OAAO;CACX;CAEA,OAAO;AACX;AAGA,SAAgB,EAAQ,GAAc,GACtC;CACI,EAAU,GAAQ,CAAM;AAC5B;AAcA,SAAgB,EACZ,GACA,GACA,GAEJ;CACI,IAAM,IAAS,GAAY,UAAU,CAAE;CAEpC,UAAW,KAAA,KAKT,aAAmB,GAKxB,OAAO;AACX;AAUA,SAAgB,EACZ,GACA,GACA,GAEJ;CACI,IAAG,KAAe,MAEd,OAAO,CAAC;CAGZ,IAAM,IAAS,EAAW,iBAAiB,CAAG;CAO9C,OALG,MAAkB,KAAA,IAEV,IAGJ,EAAO,QAAQ,MAAW,EAAO,YAAY,CAAa,CAAC;AACtE;AASA,SAAgB,EACZ,GAEJ;CACI,QAAQ,MAEG,EAAK,CAAC,EAAE,YAAY,CAAa,KAAK;AAErD;AAWA,SAAgB,EAAS,GAAa,GACtC;CACI,OAAO,IAAI,MAAM,CAAC,GAAG,EACjB,IAAI,GAAkB,GACtB;EACI,QAAQ,GAAG,MACX;GACI,IAAM,IAAS,EAAK;GAEpB,IAAG,MAAW,KAAA,GAEV,MAAU,MAAM,cAAe,EAAI,aAAc,OAAO,CAAI,EAAG,MAAM;GAKzE,OAFY,EAAO,IAER,OAAO,CAAI,EAAE,CAAC,GAAG,CAAI;EACpC;CACJ,EACJ,CAAC;AACL;;;AC5KA,SAAgB,EACZ,GACA,GAEJ;CACI,IAAM,EAAE,cAAW,EAAc,GAE3B,IAAQ,EAAS,CAAC,CAAC,GACnB,IAAU,EAAI,EAAK,GAEnB,UAEK,EAAc,EAAO,OAAO,YAAY,GAAa,CAAE,GAG5D,UACN;EACI,IAAM,IAAS,EAAK;EAEpB,IAAG,MAAW,KAAA,GACd;GAGI,EAAQ,QAAQ;GAChB;EACJ;EAGA,AADA,EAAQ,QAAQ,IAChB,EAAQ,GAAO,EAAO,KAAgC;CAC1D,GAEM,IAAM,EAAS,GAAI,CAAI,GACvB,IAAc,EAAiB,CAAI,GAErC,IAAmC;CAqBvC,OAnBA,EAAM,IAAS,MACf;EAMI,AALA,IAAa,GACb,IAAa,MAEb,EAAQ,GAEL,MAAY,SAEX,IAAa,EAAQ,SAAS,YAAY,sBAAsB,GAAS,EAAE,kBAAkB,GAAM,CAAC;CAE5G,GAAG,EAAE,WAAW,GAAK,CAAC,GAEtB,QACA;EAEI,AADA,IAAa,GACb,IAAa;CACjB,CAAC,GAEM;EACH;EACA,IAAI,UAAoB;GAAE,OAAO,EAAQ;EAAO;EACzC;EACF;EACL,YAAyC,GACzC;GACI,OAAO,EAAY,CAAa;EACpC;CACJ;AACJ;;;AClFA,SAAS,EAAkB,GAAqC,GAChE;CACI,IAAG,EAAQ,WAAW,EAAK,QAEvB,OAAO;CAGX,KAAI,IAAI,IAAQ,GAAG,IAAQ,EAAK,QAAQ,KAEpC,IAAG,EAAQ,OAAW,EAAK,IAEvB,OAAO;CAIf,OAAO;AACX;AAiBA,SAAgB,EACZ,GACA,GAEJ;CACI,IAAM,EAAE,cAAW,EAAc,GAE3B,IAAQ,EAAsC,CAAC,CAAC,GAChD,oBAAO,IAAI,IAAkB,GAE7B,KAAc,MAET,EAAO,OAAO,YAAY,UAAU,CAAE,GAG3C,KAAW,MACjB;EACI,IAAM,IAAW,EAAK,IAAI,CAAE;EAE5B,IAAG,MAAa,KAAA,GAEZ,OAAO;EAGX,IAAM,IAAQ,EAAS,CAAC,CAAC,GACnB,IAAU,EAAI,EAAI,GAClB,UAAsC,EAAW,CAAE,GACnD,IAAM,EAAS,GAAI,CAAI,GACvB,IAAc,EAAiB,CAAI,GAEnC,IAAgB;GAClB;GACA,MAAM;IACF;IACA,IAAI,UAAoB;KAAE,OAAO,EAAQ;IAAO;IACzC;IACF;IACL,YAAyC,GACzC;KACI,OAAO,EAAY,CAAK;IAC5B;GACJ;EACJ;EAIA,OAFA,EAAK,IAAI,GAAI,CAAM,GAEZ;CACX,GAEM,UACN;EACI,IAAM,IAAU,EAAc,EAAO,OAAO,YAAY,GAAK,CAAa,GACpE,IAA0B,CAAC,GAC3B,oBAAO,IAAI,IAAY;EAE7B,KAAI,IAAM,KAAU,GACpB;GACI,IAAM,IAAS,EAAQ,EAAO,EAAE;GAIhC,AAFA,EAAQ,EAAO,OAAO,EAAO,KAAgC,GAC7D,EAAK,KAAK,EAAO,IAAI,GACrB,EAAK,IAAI,EAAO,EAAE;EACtB;EAEA,KAAI,IAAM,KAAM,CAAE,GAAG,EAAK,KAAK,CAAE,GAE7B,AAAI,EAAK,IAAI,CAAE,KAEX,EAAK,OAAO,CAAE;EAItB,AAAG,EAAkB,EAAM,OAAO,CAAI,MAElC,EAAM,QAAQ;CAEtB,GAEI,IAAmC;CAqBvC,OAnBA,EAAM,IAAS,MACf;EAMI,AALA,IAAa,GACb,IAAa,MAEb,EAAQ,GAEL,MAAY,SAEX,IAAa,EAAQ,SAAS,YAAY,sBAAsB,GAAS,EAAE,kBAAkB,GAAM,CAAC;CAE5G,GAAG,EAAE,WAAW,GAAK,CAAC,GAEtB,QACA;EAEI,AADA,IAAa,GACb,IAAa;CACjB,CAAC,GAEM;AACX"}
|
package/dist/symbols.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skewedaspect/sage-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/sage-vue.js",
|
|
6
6
|
"module": "dist/sage-vue.js",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"description": "Vue 3 integration components for the SAGE game engine.",
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "vite build"
|
|
26
|
+
"build": "vite build",
|
|
27
|
+
"test": "vitest run --config vitest.config.ts",
|
|
28
|
+
"lint:types": "vue-tsc --noEmit"
|
|
27
29
|
},
|
|
28
30
|
"files": [
|
|
29
31
|
"dist"
|
|
@@ -33,10 +35,12 @@
|
|
|
33
35
|
"vue": "^3.5.0"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
38
|
+
"@skewedaspect/sage": "*",
|
|
36
39
|
"@vitejs/plugin-vue": "^6.0.3",
|
|
37
40
|
"vite": "^8.0.3",
|
|
38
41
|
"vite-plugin-dts": "^4.5.4",
|
|
39
42
|
"typescript": "^6.0.2",
|
|
40
|
-
"vue": "^3.5.27"
|
|
43
|
+
"vue": "^3.5.27",
|
|
44
|
+
"vue-tsc": "^3.3.11"
|
|
41
45
|
}
|
|
42
46
|
}
|
package/dist/useSageEvent.d.ts
DELETED
package/dist/useSageState.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { Ref } from 'vue';
|
|
2
|
-
/**
|
|
3
|
-
* Returns a reactive ref synced to an entity's state. If `key` is provided, tracks only that property; otherwise
|
|
4
|
-
* tracks the entire state object.
|
|
5
|
-
*/
|
|
6
|
-
export declare function useSageState<T = unknown>(entityId: string, key?: string): Ref<T>;
|
|
File without changes
|