@skewedaspect/sage-vue 0.10.0-beta.2 → 0.10.0-rc.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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/components/sageCanvas.vue.d.ts +19 -34
- package/dist/composables/useSageAction.d.ts +5 -0
- package/dist/composables/useSageEngine.d.ts +8 -4
- package/dist/composables/useSageEntities.d.ts +1 -1
- package/dist/composables/useSageEntity.d.ts +6 -4
- package/dist/composables/useSageEvent.d.ts +7 -4
- package/dist/composables/viewSync.d.ts +10 -18
- package/dist/index.d.ts +9 -8
- package/dist/sage-vue.css +1 -1
- package/dist/sage-vue.js +179 -130
- package/dist/sage-vue.js.map +1 -1
- package/dist/symbols.d.ts +19 -3
- package/package.json +9 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christopher Case
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @skewedaspect/sage-vue
|
|
2
|
+
|
|
3
|
+
The Vue 3 integration for `@skewedaspect/sage`: the `SageCanvas` component that boots the engine, and the
|
|
4
|
+
composables a UI reads entities, events, and timers through.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
npm install @skewedaspect/sage-vue @skewedaspect/sage vue
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Documentation: the site built from `site/docs` in https://gitlab.com/skewed-aspect/sage/sage
|
|
@@ -1,42 +1,27 @@
|
|
|
1
|
-
import { GameEngine,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { GameEngine, NodeEntityRegistry, SageOptions, ValidEntityRegistry } from '@skewedaspect/sage';
|
|
2
|
+
declare const _default: <Registry extends NodeEntityRegistry = NodeEntityRegistry>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
3
|
+
props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{
|
|
4
|
+
readonly "onEngine-ready"?: ((engine: GameEngine<Registry>) => any) | undefined;
|
|
5
|
+
readonly "onEngine-error"?: ((error: unknown) => any) | undefined;
|
|
6
|
+
} & import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps, never>, "onEngine-ready" | "onEngine-error"> & {
|
|
7
|
+
entityClasses?: Registry & ValidEntityRegistry<Registry>;
|
|
8
|
+
options?: SageOptions;
|
|
9
|
+
engine?: GameEngine<Registry>;
|
|
10
|
+
} & Partial<{}>> & import('vue').PublicProps;
|
|
11
|
+
expose(exposed: import('vue').ShallowUnwrapRef<{}>): void;
|
|
12
|
+
attrs: any;
|
|
9
13
|
slots: {
|
|
10
14
|
default?(_: {
|
|
11
15
|
loading: boolean;
|
|
12
16
|
error: unknown;
|
|
13
|
-
engine: GameEngine | null;
|
|
17
|
+
engine: GameEngine<Registry> | null;
|
|
14
18
|
}): any;
|
|
15
19
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
rootEl: HTMLDivElement;
|
|
20
|
+
emit: ((evt: "engine-ready", engine: GameEngine<Registry>) => void) & ((evt: "engine-error", error: unknown) => void);
|
|
21
|
+
}>) => import('vue').VNode & {
|
|
22
|
+
__ctx?: Awaited<typeof __VLS_setup>;
|
|
21
23
|
};
|
|
22
|
-
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
23
|
-
declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
24
|
-
"engine-ready": (engine: GameEngine) => any;
|
|
25
|
-
"engine-error": (error: unknown) => any;
|
|
26
|
-
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
27
|
-
"onEngine-ready"?: ((engine: GameEngine) => any) | undefined;
|
|
28
|
-
"onEngine-error"?: ((error: unknown) => any) | undefined;
|
|
29
|
-
}>, {
|
|
30
|
-
entityClasses: Readonly<Record<string, GameEntityType>>;
|
|
31
|
-
options: SageOptions;
|
|
32
|
-
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
|
|
33
|
-
containerRef: HTMLDivElement;
|
|
34
|
-
canvasRef: HTMLCanvasElement;
|
|
35
|
-
}, HTMLDivElement>;
|
|
36
|
-
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
37
24
|
export default _default;
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
};
|
|
25
|
+
type __VLS_PrettifyLocal<T> = {
|
|
26
|
+
[K in keyof T]: T[K];
|
|
27
|
+
} & {};
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Answers with a `Ref` holding the value an action was last delivered with, `0` until it is delivered once. An
|
|
4
|
+
* action reaches a UI as a broadcast `action:<name>` event whose payload is the value itself
|
|
5
|
+
* (design/ui.md#reading-an-action).
|
|
6
|
+
*/
|
|
2
7
|
export declare function useSageAction(actionName: string): Ref<boolean | number>;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
import { ObservedEngine } from "../symbols.js";
|
|
3
3
|
interface SageEngineContext {
|
|
4
|
-
engine:
|
|
5
|
-
eventBus: ComputedRef<GameEventBus | null>;
|
|
4
|
+
engine: Readonly<Ref<ObservedEngine | null>>;
|
|
6
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Answers with the engine the canvas above provides, in the `Ref` the canvas holds it in, so a component
|
|
8
|
+
* reads it before there is an engine and sees the one that arrives. The bus is the engine's, one per engine,
|
|
9
|
+
* and a UI takes it from the engine it already holds (design/ui.md#reaching-the-engine).
|
|
10
|
+
*/
|
|
7
11
|
export declare function useSageEngine(): SageEngineContext;
|
|
8
12
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ref } from 'vue';
|
|
2
2
|
import { BehaviorClass } from '@skewedaspect/sage';
|
|
3
|
-
import { GameEntityView, ViewNarrowedTo } from "./useSageEntity.
|
|
3
|
+
import { GameEntityView, ViewNarrowedTo } from "./useSageEntity.js";
|
|
4
4
|
/**
|
|
5
5
|
* Answers with a view for each entity carrying the tag, whatever its class, in the order the simulation
|
|
6
6
|
* holds them. A view stays the same object for as long as its entity is in the collection, so a `v-for`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
1
2
|
import { BehaviorClass, DeepReadonly, GameEntityType, OpsFor, OpsOf, StateFor, StateSliceOf } from '@skewedaspect/sage';
|
|
2
3
|
/** What a UI observes an entity through (design/ui.md#gameentityview). */
|
|
3
4
|
export interface GameEntityView<Type extends GameEntityType = GameEntityType> {
|
|
@@ -13,11 +14,12 @@ export type ViewNarrowedTo<Class extends BehaviorClass> = GameEntityView & {
|
|
|
13
14
|
readonly ops: OpsOf<Class>;
|
|
14
15
|
};
|
|
15
16
|
/**
|
|
16
|
-
* Answers with a view of one entity, resolved by class and ID against the simulation the
|
|
17
|
-
* holds. The view observes as it is created and refreshes once per rendered frame, pause
|
|
18
|
-
* (design/ui.md#when-a-view-observes).
|
|
17
|
+
* Answers with a `Ref` holding a view of one entity, resolved by class and ID against the simulation the
|
|
18
|
+
* engine currently holds. The view observes as it is created and refreshes once per rendered frame, pause
|
|
19
|
+
* included (design/ui.md#when-a-view-observes). The `Ref` never changes what it holds, since the class and
|
|
20
|
+
* the ID are fixed for its life; what a refresh changes is inside the view (design/ui.md#gameentityview).
|
|
19
21
|
*
|
|
20
22
|
* @param entityClass - Types the view and checks it: another class leaves the view absent.
|
|
21
23
|
* @param id - The entity ID the view resolves on every refresh.
|
|
22
24
|
*/
|
|
23
|
-
export declare function useSageEntity<Type extends GameEntityType>(entityClass: Type, id: string): GameEntityView<Type
|
|
25
|
+
export declare function useSageEntity<Type extends GameEntityType>(entityClass: Type, id: string): Ref<GameEntityView<Type>>;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { EventCallback, EventSubscription, Unsubscribe } from '@skewedaspect/sage';
|
|
2
|
+
/**
|
|
3
|
+
* Subscribes a handler on the engine bus for as long as the component is mounted, and answers with the
|
|
4
|
+
* teardown for a component whose subscription ends before it does. The subscription is an event type in
|
|
5
|
+
* the map or a pattern over it, and the handler runs from a drain (design/ui.md#hearing-events).
|
|
6
|
+
*/
|
|
7
|
+
export declare function useSageEvent<Subscription extends EventSubscription>(subscription: Subscription, handler: EventCallback<Subscription>): Unsubscribe;
|
|
@@ -1,39 +1,31 @@
|
|
|
1
|
-
import { BehaviorClass, GameEntity, GameEntityType
|
|
1
|
+
import { BehaviorClass, GameEntity, GameEntityType } from '@skewedaspect/sage';
|
|
2
|
+
import { ObservedSimulation } from "../symbols.js";
|
|
2
3
|
type Bag = Record<string, unknown>;
|
|
4
|
+
/** The bag a view holds its state in: reactive for a template, and with no prototype the way its records are. */
|
|
5
|
+
export declare function createStateBag(): Bag;
|
|
3
6
|
/** Assigns what differs, adds what appeared, removes what vanished, and leaves everything else alone. */
|
|
4
7
|
export declare function syncBag(target: Bag, source: Readonly<Bag>): void;
|
|
5
8
|
/**
|
|
6
9
|
* Answers with the entity that ID names in the simulation the engine currently holds, when it is of the class
|
|
7
10
|
* asked for. A subclass is of that class (design/ui.md#gameentityview).
|
|
8
|
-
*
|
|
9
|
-
* @param simulation
|
|
10
|
-
* @param entityClass
|
|
11
|
-
* @param id
|
|
12
11
|
*/
|
|
13
|
-
export declare function resolveEntity(simulation:
|
|
12
|
+
export declare function resolveEntity(simulation: ObservedSimulation | null | undefined, entityClass: GameEntityType, id: string): GameEntity | undefined;
|
|
14
13
|
/**
|
|
15
14
|
* Answers with the entities carrying the tag, whatever their class, in the order the simulation holds them.
|
|
16
15
|
* A behavior narrows that to the ones holding it (design/ui.md#gameentityview).
|
|
17
|
-
*
|
|
18
|
-
* @param simulation
|
|
19
|
-
* @param tag
|
|
20
|
-
* @param behaviorClass
|
|
21
16
|
*/
|
|
22
|
-
export declare function resolveTagged(simulation:
|
|
17
|
+
export declare function resolveTagged(simulation: ObservedSimulation | null | undefined, tag: string, behaviorClass?: BehaviorClass): readonly GameEntity[];
|
|
23
18
|
/**
|
|
24
19
|
* Builds the narrowing guard a view exposes. It resolves the entity on every call, so it answers from the
|
|
25
20
|
* entity rather than from the view's copy of state, and a view whose entity has gone holds no behavior
|
|
26
21
|
* (design/ui.md#gameentityview).
|
|
27
|
-
*
|
|
28
|
-
* @param find
|
|
29
22
|
*/
|
|
30
23
|
export declare function buildHasBehavior(find: () => GameEntity | undefined): (behaviorClass: BehaviorClass) => boolean;
|
|
31
24
|
/**
|
|
32
|
-
* Builds the
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @param find
|
|
25
|
+
* Builds the operations a view exposes. Every call resolves the entity again, so a view whose entity has gone
|
|
26
|
+
* throws rather than reaching the one it used to hold (design/ui.md#absence). One name answers with one
|
|
27
|
+
* function for the view's life, so a template handing an operation to a child hands it the same function on
|
|
28
|
+
* every render (design/ui.md#gameentityview).
|
|
37
29
|
*/
|
|
38
30
|
export declare function buildOps(id: string, find: () => GameEntity | undefined): Record<string, unknown>;
|
|
39
31
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { default as SageCanvas } from './components/sageCanvas.vue';
|
|
2
|
-
export { useSageEngine } from "./composables/useSageEngine.
|
|
3
|
-
export { useSageEvent } from "./composables/useSageEvent.
|
|
4
|
-
export { useSageAction } from "./composables/useSageAction.
|
|
5
|
-
export { useSageTimer } from "./composables/useSageTimer.
|
|
6
|
-
export { useSageEntity } from "./composables/useSageEntity.
|
|
7
|
-
export type { GameEntityView, ViewNarrowedTo } from "./composables/useSageEntity.
|
|
8
|
-
export { useSageEntities } from "./composables/useSageEntities.
|
|
9
|
-
export { SAGE_ENGINE_KEY } from "./symbols.
|
|
2
|
+
export { useSageEngine } from "./composables/useSageEngine.js";
|
|
3
|
+
export { useSageEvent } from "./composables/useSageEvent.js";
|
|
4
|
+
export { useSageAction } from "./composables/useSageAction.js";
|
|
5
|
+
export { useSageTimer } from "./composables/useSageTimer.js";
|
|
6
|
+
export { useSageEntity } from "./composables/useSageEntity.js";
|
|
7
|
+
export type { GameEntityView, ViewNarrowedTo } from "./composables/useSageEntity.js";
|
|
8
|
+
export { useSageEntities } from "./composables/useSageEntities.js";
|
|
9
|
+
export { SAGE_ENGINE_KEY } from "./symbols.js";
|
|
10
|
+
export type { ObservedEngine, ObservedSimulation } from "./symbols.js";
|
package/dist/sage-vue.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.sage-container[data-v-
|
|
1
|
+
.sage-container[data-v-00af6556]{width:100%;height:100%;position:relative}.sage-canvas[data-v-00af6556]{width:100%;height:100%;display:block}.sage-overlay[data-v-00af6556]{pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0}.sage-overlay[data-v-00af6556] *{pointer-events:auto}
|
|
2
2
|
/*$vite$:1*/
|
package/dist/sage-vue.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { computed as e, createElementBlock as t, createElementVNode as n, defineComponent as r,
|
|
2
|
-
import { createGameEngine as
|
|
1
|
+
import { computed as e, createElementBlock as t, createElementVNode as n, defineComponent as r, effectScope as i, getCurrentInstance as a, inject as o, onMounted as s, onUnmounted as c, openBlock as l, provide as u, reactive as d, ref as f, renderSlot as p, shallowRef as m, useTemplateRef as h, watch as g } from "vue";
|
|
2
|
+
import { createGameEngine as _ } from "@skewedaspect/sage";
|
|
3
3
|
//#region src/symbols.ts
|
|
4
|
-
var
|
|
4
|
+
var v = Symbol("sage-engine"), y = { class: "sage-overlay" }, b = /*#__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;
|
|
@@ -13,252 +13,301 @@ var g = Symbol("sage-engine"), _ = { class: "sage-overlay" }, v = /*#__PURE__*/
|
|
|
13
13
|
engine: {}
|
|
14
14
|
},
|
|
15
15
|
emits: ["engine-ready", "engine-error"],
|
|
16
|
-
setup(
|
|
17
|
-
let
|
|
18
|
-
|
|
16
|
+
setup(r, { emit: i }) {
|
|
17
|
+
let o = r, d = i, g = h("containerRef"), b = h("canvasRef"), x = m(null), S = f(!0), C = f(null), w = !1, T = null, E = a();
|
|
18
|
+
u(v, e(() => x.value));
|
|
19
|
+
function D(e) {
|
|
20
|
+
let t = E?.vnode.props?.onEngineError;
|
|
21
|
+
t?.(e);
|
|
22
|
+
}
|
|
23
|
+
return s(async () => {
|
|
19
24
|
let e = !1;
|
|
20
|
-
if (
|
|
25
|
+
if (c(() => {
|
|
21
26
|
e = !0;
|
|
22
|
-
}),
|
|
27
|
+
}), b.value) try {
|
|
23
28
|
let t;
|
|
24
|
-
if (
|
|
25
|
-
|
|
29
|
+
if (o.engine ? (t = o.engine, w = !0) : t = await _(b.value, o.entityClasses, o.options), e) {
|
|
30
|
+
if (!w) {
|
|
31
|
+
try {
|
|
32
|
+
await t.stop();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
D(e);
|
|
35
|
+
}
|
|
36
|
+
t.renderEngine.dispose();
|
|
37
|
+
}
|
|
26
38
|
return;
|
|
27
39
|
}
|
|
28
|
-
|
|
40
|
+
x.value = t, g.value && (T = new ResizeObserver(() => {
|
|
29
41
|
t.renderEngine.resize();
|
|
30
|
-
}),
|
|
42
|
+
}), T.observe(g.value)), d("engine-ready", t), S.value = !1;
|
|
31
43
|
} catch (t) {
|
|
32
44
|
if (e) return;
|
|
33
|
-
|
|
45
|
+
C.value = t, d("engine-error", t), S.value = !1;
|
|
46
|
+
}
|
|
47
|
+
}), c(async () => {
|
|
48
|
+
if (T &&= (T.disconnect(), null), !w && x.value) {
|
|
49
|
+
try {
|
|
50
|
+
await x.value.stop();
|
|
51
|
+
} catch (e) {
|
|
52
|
+
D(e);
|
|
53
|
+
}
|
|
54
|
+
x.value.renderEngine.dispose();
|
|
34
55
|
}
|
|
35
|
-
}),
|
|
36
|
-
C &&= (C.disconnect(), null), !S && y.value && (await y.value.stop(), y.value.renderEngine.dispose());
|
|
37
|
-
}), (e, r) => (s(), t("div", {
|
|
56
|
+
}), (e, r) => (l(), t("div", {
|
|
38
57
|
ref_key: "containerRef",
|
|
39
|
-
ref:
|
|
58
|
+
ref: g,
|
|
40
59
|
class: "sage-container"
|
|
41
60
|
}, [n("canvas", {
|
|
42
61
|
ref_key: "canvasRef",
|
|
43
|
-
ref:
|
|
62
|
+
ref: b,
|
|
44
63
|
class: "sage-canvas"
|
|
45
|
-
}, null, 512), n("div",
|
|
46
|
-
loading:
|
|
47
|
-
error:
|
|
48
|
-
engine:
|
|
64
|
+
}, null, 512), n("div", y, [p(e.$slots, "default", {
|
|
65
|
+
loading: S.value,
|
|
66
|
+
error: C.value,
|
|
67
|
+
engine: x.value
|
|
49
68
|
}, void 0, !0)])], 512));
|
|
50
69
|
}
|
|
51
|
-
}), [["__scopeId", "data-v-
|
|
70
|
+
}), [["__scopeId", "data-v-00af6556"]]);
|
|
52
71
|
//#endregion
|
|
53
72
|
//#region src/composables/useSageEngine.ts
|
|
54
|
-
function
|
|
55
|
-
let
|
|
56
|
-
if (!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
73
|
+
function x() {
|
|
74
|
+
let e = o(v);
|
|
75
|
+
if (!e) {
|
|
76
|
+
let e = a()?.type.name, t = e ? `<${e}>` : "A component";
|
|
77
|
+
throw Error(`${t} called useSageEngine() with no <SageCanvas> above it.`);
|
|
78
|
+
}
|
|
79
|
+
return { engine: e };
|
|
61
80
|
}
|
|
62
81
|
//#endregion
|
|
63
82
|
//#region src/composables/useSageEvent.ts
|
|
64
|
-
function
|
|
65
|
-
let { engine: n } =
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
function S(e, t) {
|
|
84
|
+
let { engine: n } = x(), r = null, i = () => {
|
|
85
|
+
r !== null && (r(), r = null);
|
|
86
|
+
}, a = g(n, (n) => {
|
|
87
|
+
i(), n && (r = n.bus.on(e, t));
|
|
68
88
|
}, {
|
|
69
89
|
immediate: !0,
|
|
70
90
|
flush: "sync"
|
|
71
|
-
}), o(
|
|
72
|
-
|
|
73
|
-
}
|
|
91
|
+
}), o = () => {
|
|
92
|
+
a(), i();
|
|
93
|
+
};
|
|
94
|
+
return c(o), o;
|
|
74
95
|
}
|
|
75
96
|
//#endregion
|
|
76
97
|
//#region src/composables/useSageAction.ts
|
|
77
|
-
function
|
|
78
|
-
let t =
|
|
79
|
-
return
|
|
80
|
-
t.value = e.payload
|
|
98
|
+
function C(e) {
|
|
99
|
+
let t = f(0);
|
|
100
|
+
return S(`action:${e}`, (e) => {
|
|
101
|
+
t.value = e.payload;
|
|
81
102
|
}), t;
|
|
82
103
|
}
|
|
83
104
|
//#endregion
|
|
84
105
|
//#region src/composables/useSageTimer.ts
|
|
85
|
-
function
|
|
86
|
-
let { engine: e } =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
function w() {
|
|
107
|
+
let { engine: e } = x(), t = [], n = /* @__PURE__ */ new Set(), r = /* @__PURE__ */ new Set(), a = i(!0);
|
|
108
|
+
a.run(() => {
|
|
109
|
+
g(e, (e) => {
|
|
110
|
+
if (!e) return;
|
|
111
|
+
let t = [...n];
|
|
112
|
+
n.clear();
|
|
113
|
+
for (let n of t) n(e);
|
|
114
|
+
let i = [...r];
|
|
115
|
+
r.clear();
|
|
116
|
+
for (let t of i) t(e);
|
|
117
|
+
a.stop();
|
|
118
|
+
}, {
|
|
119
|
+
immediate: !0,
|
|
120
|
+
flush: "sync"
|
|
121
|
+
});
|
|
122
|
+
}), c(() => {
|
|
96
123
|
n.clear();
|
|
97
124
|
for (let e of t) e();
|
|
98
|
-
t.length = 0;
|
|
125
|
+
t.length = 0, r.size === 0 && a.stop();
|
|
99
126
|
});
|
|
100
|
-
function
|
|
127
|
+
function o(e) {
|
|
101
128
|
e();
|
|
102
129
|
let n = t.indexOf(e);
|
|
103
130
|
n >= 0 && t.splice(n, 1);
|
|
104
131
|
}
|
|
105
|
-
function
|
|
106
|
-
let
|
|
107
|
-
|
|
132
|
+
function s(r) {
|
|
133
|
+
let i = null, a = (e) => {
|
|
134
|
+
i = r(e), t.push(i);
|
|
108
135
|
};
|
|
109
|
-
return e.value ?
|
|
110
|
-
n.delete(
|
|
136
|
+
return e.value ? a(e.value) : n.add(a), () => {
|
|
137
|
+
n.delete(a), i &&= (o(i), null);
|
|
111
138
|
};
|
|
112
139
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
140
|
+
function l(e, t) {
|
|
141
|
+
if (e < 0) throw RangeError(`delay() takes zero or more milliseconds of game time, and was given ${e}.`);
|
|
142
|
+
return s((n) => n.timer.delay(e, t));
|
|
115
143
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
144
|
+
function u(e, t) {
|
|
145
|
+
if (e <= 0) throw RangeError(`interval() takes a positive number of milliseconds of game time, and was given ${e}.`);
|
|
146
|
+
return s((n) => n.timer.interval(e, t));
|
|
118
147
|
}
|
|
119
|
-
function
|
|
148
|
+
function d(t) {
|
|
120
149
|
if (e.value) return e.value.timer.cooldown(t);
|
|
121
|
-
let
|
|
122
|
-
return
|
|
123
|
-
|
|
150
|
+
let n = null, i = !1;
|
|
151
|
+
return r.add((e) => {
|
|
152
|
+
n = e.timer.cooldown(t), i &&= (n.reset(), !1);
|
|
124
153
|
}), {
|
|
125
154
|
get ready() {
|
|
126
|
-
return
|
|
155
|
+
return n ? n.ready : !1;
|
|
127
156
|
},
|
|
128
157
|
reset() {
|
|
129
|
-
|
|
158
|
+
n ? n.reset() : i = !0;
|
|
130
159
|
}
|
|
131
160
|
};
|
|
132
161
|
}
|
|
133
162
|
return {
|
|
134
|
-
delay:
|
|
135
|
-
interval:
|
|
136
|
-
cooldown:
|
|
163
|
+
delay: l,
|
|
164
|
+
interval: u,
|
|
165
|
+
cooldown: d
|
|
137
166
|
};
|
|
138
167
|
}
|
|
139
168
|
//#endregion
|
|
140
169
|
//#region src/composables/viewSync.ts
|
|
141
|
-
function
|
|
142
|
-
|
|
170
|
+
function T() {
|
|
171
|
+
return d(Object.create(null));
|
|
172
|
+
}
|
|
173
|
+
function E(e) {
|
|
174
|
+
if (Array.isArray(e)) return e.map((e) => E(e));
|
|
143
175
|
if (typeof e == "object" && e) {
|
|
144
|
-
let t =
|
|
145
|
-
for (let [n, r] of Object.entries(e)) t[n] =
|
|
176
|
+
let t = Object.create(null);
|
|
177
|
+
for (let [n, r] of Object.entries(e)) t[n] = E(r);
|
|
146
178
|
return t;
|
|
147
179
|
}
|
|
148
180
|
return e;
|
|
149
181
|
}
|
|
150
|
-
function
|
|
182
|
+
function D(e, t) {
|
|
151
183
|
if (Array.isArray(t)) {
|
|
152
|
-
if (!Array.isArray(e)) return
|
|
184
|
+
if (!Array.isArray(e)) return E(t);
|
|
153
185
|
e.length !== t.length && (e.length = t.length);
|
|
154
|
-
for (let [n, r] of t.entries()) e[n] =
|
|
186
|
+
for (let [n, r] of t.entries()) e[n] = D(e[n], r);
|
|
155
187
|
return e;
|
|
156
188
|
}
|
|
157
189
|
if (typeof t == "object" && t) {
|
|
158
|
-
if (typeof e != "object" || !e || Array.isArray(e)) return
|
|
190
|
+
if (typeof e != "object" || !e || Array.isArray(e)) return E(t);
|
|
159
191
|
let n = e, r = t;
|
|
160
192
|
for (let e of Object.keys(n)) e in r || Reflect.deleteProperty(n, e);
|
|
161
193
|
for (let [e, t] of Object.entries(r)) {
|
|
162
|
-
let r =
|
|
194
|
+
let r = D(n[e], t);
|
|
163
195
|
r !== n[e] && (n[e] = r);
|
|
164
196
|
}
|
|
165
197
|
return e;
|
|
166
198
|
}
|
|
167
199
|
return t;
|
|
168
200
|
}
|
|
169
|
-
function
|
|
170
|
-
|
|
201
|
+
function O(e, t) {
|
|
202
|
+
D(e, t);
|
|
171
203
|
}
|
|
172
|
-
function
|
|
204
|
+
function k(e, t, n) {
|
|
173
205
|
let r = e?.getEntity(n);
|
|
174
206
|
if (r !== void 0 && r instanceof t) return r;
|
|
175
207
|
}
|
|
176
|
-
function
|
|
208
|
+
function A(e, t, n) {
|
|
177
209
|
if (e == null) return [];
|
|
178
210
|
let r = e.getEntitiesByTag(t);
|
|
179
211
|
return n === void 0 ? r : r.filter((e) => e.hasBehavior(n));
|
|
180
212
|
}
|
|
181
|
-
function
|
|
213
|
+
function j(e) {
|
|
182
214
|
return (t) => e()?.hasBehavior(t) ?? !1;
|
|
183
215
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
var M = /* @__PURE__ */ new Set([
|
|
217
|
+
"then",
|
|
218
|
+
"__v_isRef",
|
|
219
|
+
"__v_isReactive",
|
|
220
|
+
"__v_isReadonly",
|
|
221
|
+
"__v_raw",
|
|
222
|
+
"__v_skip"
|
|
223
|
+
]);
|
|
224
|
+
function N(e, t) {
|
|
225
|
+
let n = /* @__PURE__ */ new Map();
|
|
226
|
+
return new Proxy({}, { get(r, i) {
|
|
227
|
+
if (typeof i == "symbol" || M.has(i)) return;
|
|
228
|
+
let a = n.get(i);
|
|
229
|
+
if (a !== void 0) return a;
|
|
230
|
+
let o = (...n) => {
|
|
231
|
+
let r = t();
|
|
232
|
+
if (r === void 0) throw Error(`No entity "${e}" to call "${i}" on.`);
|
|
233
|
+
let a = r.ops;
|
|
234
|
+
if (!Object.hasOwn(a, i)) throw Error(`Entity "${e}" exposes no operation "${i}".`);
|
|
235
|
+
return a[i](...n);
|
|
190
236
|
};
|
|
237
|
+
return n.set(i, o), o;
|
|
191
238
|
} });
|
|
192
239
|
}
|
|
193
240
|
//#endregion
|
|
194
241
|
//#region src/composables/useSageEntity.ts
|
|
195
|
-
function
|
|
196
|
-
let { engine: n } =
|
|
242
|
+
function P(e, t) {
|
|
243
|
+
let { engine: n } = x(), r = T(), i = f(!1), a = () => k(n.value?.simulation, e, t), o = () => {
|
|
197
244
|
let e = a();
|
|
198
245
|
if (e === void 0) {
|
|
199
246
|
i.value = !1;
|
|
200
247
|
return;
|
|
201
248
|
}
|
|
202
|
-
i.value = !0,
|
|
203
|
-
},
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
}, { immediate: !0 }),
|
|
207
|
-
|
|
208
|
-
}), {
|
|
249
|
+
i.value = !0, O(r, e.state);
|
|
250
|
+
}, s = N(t, a), l = j(a), u = null;
|
|
251
|
+
return g(n, (e) => {
|
|
252
|
+
u?.(), u = null, o(), e !== null && (u = e.managers.gameManager.registerFrameCallback(o, { suspendedByPause: !1 }));
|
|
253
|
+
}, { immediate: !0 }), c(() => {
|
|
254
|
+
u?.(), u = null;
|
|
255
|
+
}), m({
|
|
209
256
|
id: t,
|
|
210
257
|
get present() {
|
|
211
258
|
return i.value;
|
|
212
259
|
},
|
|
213
260
|
state: r,
|
|
214
|
-
ops:
|
|
261
|
+
ops: s,
|
|
215
262
|
hasBehavior(e) {
|
|
216
|
-
return
|
|
263
|
+
return l(e);
|
|
217
264
|
}
|
|
218
|
-
};
|
|
265
|
+
});
|
|
219
266
|
}
|
|
220
267
|
//#endregion
|
|
221
268
|
//#region src/composables/useSageEntities.ts
|
|
222
|
-
function
|
|
269
|
+
function F(e, t) {
|
|
223
270
|
if (e.length !== t.length) return !0;
|
|
224
271
|
for (let n = 0; n < t.length; n++) if (e[n] !== t[n]) return !0;
|
|
225
272
|
return !1;
|
|
226
273
|
}
|
|
227
|
-
function
|
|
228
|
-
let { engine: n } =
|
|
274
|
+
function I(e, t) {
|
|
275
|
+
let { engine: n } = x(), r = m([]), i = /* @__PURE__ */ new Map(), a = (e) => {
|
|
276
|
+
if (i.has(e)) return n.value?.simulation?.getEntity(e);
|
|
277
|
+
}, o = (e) => {
|
|
229
278
|
let t = i.get(e);
|
|
230
279
|
if (t !== void 0) return t;
|
|
231
|
-
let n =
|
|
280
|
+
let n = T(), o = () => a(e), s = N(e, o), c = j(o), l = {
|
|
281
|
+
id: e,
|
|
282
|
+
get present() {
|
|
283
|
+
return r.value.includes(l);
|
|
284
|
+
},
|
|
232
285
|
state: n,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return r.value;
|
|
237
|
-
},
|
|
238
|
-
state: n,
|
|
239
|
-
ops: s,
|
|
240
|
-
hasBehavior(e) {
|
|
241
|
-
return c(e);
|
|
242
|
-
}
|
|
286
|
+
ops: s,
|
|
287
|
+
hasBehavior(e) {
|
|
288
|
+
return c(e);
|
|
243
289
|
}
|
|
290
|
+
}, u = {
|
|
291
|
+
state: n,
|
|
292
|
+
view: l
|
|
244
293
|
};
|
|
245
|
-
return i.set(e,
|
|
246
|
-
},
|
|
247
|
-
let a =
|
|
294
|
+
return i.set(e, u), u;
|
|
295
|
+
}, s = () => {
|
|
296
|
+
let a = A(n.value?.simulation, e, t), s = [], c = /* @__PURE__ */ new Set();
|
|
248
297
|
for (let e of a) {
|
|
249
|
-
let t =
|
|
250
|
-
|
|
298
|
+
let t = o(e.id);
|
|
299
|
+
O(t.state, e.state), s.push(t.view), c.add(e.id);
|
|
251
300
|
}
|
|
252
301
|
for (let e of [...i.keys()]) c.has(e) || i.delete(e);
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
return
|
|
256
|
-
|
|
257
|
-
}, { immediate: !0 }),
|
|
258
|
-
|
|
302
|
+
F(r.value, s) && (r.value = s);
|
|
303
|
+
}, l = null;
|
|
304
|
+
return g(n, (e) => {
|
|
305
|
+
l?.(), l = null, s(), e !== null && (l = e.managers.gameManager.registerFrameCallback(s, { suspendedByPause: !1 }));
|
|
306
|
+
}, { immediate: !0 }), c(() => {
|
|
307
|
+
l?.(), l = null;
|
|
259
308
|
}), r;
|
|
260
309
|
}
|
|
261
310
|
//#endregion
|
|
262
|
-
export {
|
|
311
|
+
export { v as SAGE_ENGINE_KEY, b as SageCanvas, C as useSageAction, x as useSageEngine, I as useSageEntities, P as useSageEntity, S as useSageEvent, w as useSageTimer };
|
|
263
312
|
|
|
264
313
|
//# 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/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":"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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, Ref } from 'vue';\nimport type { GameEngine, GameManager, GameSimulationManager } from '@skewedaspect/sage';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * What sage-vue reads of a simulation: its two entity lookups. GameSimulationManager is invariant in its registry\n * type parameter, so naming only the members whose signature does not mention the registry is what lets a\n * simulation built from any registry satisfy this (design/ui.md#reaching-the-engine).\n */\nexport type ObservedSimulation = Pick<GameSimulationManager, 'getEntity' | 'getEntitiesByTag'>;\n\n/**\n * What sage-vue reads of an engine: enough to hear the bus, run timers, look entities up, and refresh views\n * once a frame. An engine typed by any entity registry is one of these (design/ui.md#reaching-the-engine).\n */\nexport type ObservedEngine = Pick<GameEngine, 'bus' | 'timer' | 'renderEngine' | 'stop'> & {\n readonly simulation : ObservedSimulation | null;\n readonly managers : { readonly gameManager : Pick<GameManager, 'registerFrameCallback'> };\n};\n\nexport const SAGE_ENGINE_KEY : InjectionKey<Readonly<Ref<ObservedEngine | 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\" generic=\"Registry extends NodeEntityRegistry = NodeEntityRegistry\">\n import {\n computed, getCurrentInstance, onMounted, onUnmounted, provide, ref, shallowRef, useTemplateRef,\n } from 'vue';\n import { createGameEngine } from '@skewedaspect/sage';\n import type { GameEngine, NodeEntityRegistry, SageOptions, ValidEntityRegistry } from '@skewedaspect/sage';\n import type { ObservedEngine } from '../symbols.ts';\n import { SAGE_ENGINE_KEY } from '../symbols.ts';\n\n //------------------------------------------------------------------------------------------------------------------\n // Props & Emits\n //------------------------------------------------------------------------------------------------------------------\n\n // Generic in the entity registry, so a game that lets the canvas boot the engine gets it back typed by the\n // registry it handed in (design/engine.md#booting-an-engine).\n const props = withDefaults(defineProps<{\n entityClasses ?: Registry & ValidEntityRegistry<Registry>;\n options ?: SageOptions;\n engine ?: GameEngine<Registry>;\n }>(), {\n entityClasses: () => ({} as Registry & ValidEntityRegistry<Registry>),\n options: () => ({}),\n });\n\n const emit = defineEmits<{\n 'engine-ready' : [engine : GameEngine<Registry>];\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<Registry> | 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 const instance = getCurrentInstance();\n\n // Provide the engine ref synchronously so child composables can inject it\n // The composables below read the engine, never replace it (design/ui.md#reaching-the-engine).\n provide(SAGE_ENGINE_KEY, computed<ObservedEngine | null>(() => engineRef.value));\n\n /**\n * A stop failure while disposing an engine the component owns is announced with engine-error, whether\n * that happens as the component unmounts or as it disposes an engine that finished booting after it\n * already went away. By the time either awaited stop() settles, Vue has already marked the instance\n * unmounted, which turns emit() into a silent no-op, so this reaches the listener through the prop\n * emit() itself compiles down to instead (design/ui.md#the-canvas).\n */\n function announceEngineError(err : unknown) : void\n {\n const vnodeProps = instance?.vnode.props as Record<string, unknown> | null | undefined;\n const onEngineError = vnodeProps?.onEngineError as ((error : unknown) => void) | undefined;\n\n onEngineError?.(err);\n }\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<Registry>;\n\n if(props.engine)\n {\n engine = props.engine;\n controlled = true;\n }\n else\n {\n // withDefaults wraps the prop's type in its own NotUndefined, which the call has to see through.\n engine = await createGameEngine<Registry>(\n canvasRef.value,\n props.entityClasses as Registry & ValidEntityRegistry<Registry>,\n props.options\n );\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 try\n {\n await engine.stop();\n }\n catch(stopErr)\n {\n announceEngineError(stopErr);\n }\n\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 try\n {\n await engineRef.value.stop();\n }\n catch(err)\n {\n announceEngineError(err);\n }\n\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\" generic=\"Registry extends NodeEntityRegistry = NodeEntityRegistry\">\n import {\n computed, getCurrentInstance, onMounted, onUnmounted, provide, ref, shallowRef, useTemplateRef,\n } from 'vue';\n import { createGameEngine } from '@skewedaspect/sage';\n import type { GameEngine, NodeEntityRegistry, SageOptions, ValidEntityRegistry } from '@skewedaspect/sage';\n import type { ObservedEngine } from '../symbols.ts';\n import { SAGE_ENGINE_KEY } from '../symbols.ts';\n\n //------------------------------------------------------------------------------------------------------------------\n // Props & Emits\n //------------------------------------------------------------------------------------------------------------------\n\n // Generic in the entity registry, so a game that lets the canvas boot the engine gets it back typed by the\n // registry it handed in (design/engine.md#booting-an-engine).\n const props = withDefaults(defineProps<{\n entityClasses ?: Registry & ValidEntityRegistry<Registry>;\n options ?: SageOptions;\n engine ?: GameEngine<Registry>;\n }>(), {\n entityClasses: () => ({} as Registry & ValidEntityRegistry<Registry>),\n options: () => ({}),\n });\n\n const emit = defineEmits<{\n 'engine-ready' : [engine : GameEngine<Registry>];\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<Registry> | 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 const instance = getCurrentInstance();\n\n // Provide the engine ref synchronously so child composables can inject it\n // The composables below read the engine, never replace it (design/ui.md#reaching-the-engine).\n provide(SAGE_ENGINE_KEY, computed<ObservedEngine | null>(() => engineRef.value));\n\n /**\n * A stop failure while disposing an engine the component owns is announced with engine-error, whether\n * that happens as the component unmounts or as it disposes an engine that finished booting after it\n * already went away. By the time either awaited stop() settles, Vue has already marked the instance\n * unmounted, which turns emit() into a silent no-op, so this reaches the listener through the prop\n * emit() itself compiles down to instead (design/ui.md#the-canvas).\n */\n function announceEngineError(err : unknown) : void\n {\n const vnodeProps = instance?.vnode.props as Record<string, unknown> | null | undefined;\n const onEngineError = vnodeProps?.onEngineError as ((error : unknown) => void) | undefined;\n\n onEngineError?.(err);\n }\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<Registry>;\n\n if(props.engine)\n {\n engine = props.engine;\n controlled = true;\n }\n else\n {\n // withDefaults wraps the prop's type in its own NotUndefined, which the call has to see through.\n engine = await createGameEngine<Registry>(\n canvasRef.value,\n props.entityClasses as Registry & ValidEntityRegistry<Registry>,\n props.options\n );\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 try\n {\n await engine.stop();\n }\n catch(stopErr)\n {\n announceEngineError(stopErr);\n }\n\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 try\n {\n await engineRef.value.stop();\n }\n catch(err)\n {\n announceEngineError(err);\n }\n\n engineRef.value.renderEngine.dispose();\n }\n });\n</script>\n\n<!--------------------------------------------------------------------------------------------------------------------->\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEngine Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, getCurrentInstance, inject } from 'vue';\n\nimport { SAGE_ENGINE_KEY } from '../symbols.ts';\nimport type { ObservedEngine } from '../symbols.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ninterface SageEngineContext\n{\n engine : Readonly<Ref<ObservedEngine | null>>;\n}\n\n/**\n * Answers with the engine the canvas above provides, in the `Ref` the canvas holds it in, so a component\n * reads it before there is an engine and sees the one that arrives. The bus is the engine's, one per engine,\n * and a UI takes it from the engine it already holds (design/ui.md#reaching-the-engine).\n */\nexport function useSageEngine() : SageEngineContext\n{\n const engine = inject(SAGE_ENGINE_KEY);\n\n if(!engine)\n {\n const name = getCurrentInstance()?.type.name;\n const caller = name ? `<${ name }>` : 'A component';\n\n throw new Error(`${ caller } called useSageEngine() with no <SageCanvas> above it.`);\n }\n\n return { engine };\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEvent Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { onUnmounted, watch } from 'vue';\nimport type { EventCallback, EventSubscription, Unsubscribe } from '@skewedaspect/sage';\n\nimport { useSageEngine } from './useSageEngine.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Subscribes a handler on the engine bus for as long as the component is mounted, and answers with the\n * teardown for a component whose subscription ends before it does. The subscription is an event type in\n * the map or a pattern over it, and the handler runs from a drain (design/ui.md#hearing-events).\n */\nexport function useSageEvent<Subscription extends EventSubscription>(\n subscription : Subscription,\n handler : EventCallback<Subscription>\n) : Unsubscribe\n{\n const { engine } = useSageEngine();\n let unsubscribe : Unsubscribe | null = null;\n\n const drop = () : void =>\n {\n if(unsubscribe !== null)\n {\n unsubscribe();\n unsubscribe = null;\n }\n };\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 emitted in that window.\n const stopWatching = watch(engine, (current) =>\n {\n drop();\n\n if(current)\n {\n unsubscribe = current.bus.on(subscription, handler);\n }\n }, { immediate: true, flush: 'sync' });\n\n const end = () : void =>\n {\n stopWatching();\n drop();\n };\n\n onUnmounted(end);\n\n return end;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageAction Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, ref } from 'vue';\nimport type { EventSubscription } from '@skewedaspect/sage';\n\nimport { useSageEvent } from './useSageEvent.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\n/**\n * Answers with a `Ref` holding the value an action was last delivered with, `0` until it is delivered once. An\n * action reaches a UI as a broadcast `action:<name>` event whose payload is the value itself\n * (design/ui.md#reading-an-action).\n */\nexport function useSageAction(actionName : string) : Ref<boolean | number>\n{\n const value = ref<boolean | number>(0);\n\n // The name is a runtime string, so the subscription is typed as the map's by hand; an action outside the map\n // hears nothing.\n useSageEvent(`action:${ actionName }` as EventSubscription, (event) =>\n {\n value.value = event.payload as unknown as boolean | number;\n });\n\n return value;\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageTimer Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { effectScope, onUnmounted, watch } from 'vue';\nimport type { CooldownHandle } from '@skewedaspect/sage';\nimport type { ObservedEngine } from '../symbols.ts';\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 : ObservedEngine) => 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\n // Unmounting drops a queued delay or interval, but leaves a queued cooldown waiting (design/ui.md#timers),\n // so the two kinds are tracked apart.\n const pendingTimers = new Set<PendingTimer>();\n const pendingCooldowns = new Set<PendingTimer>();\n\n // Detached, so this keeps watching for the engine past the component's own unmount -- a queued cooldown\n // still has to bind once the engine boots, however long after the component that asked for it is gone\n // (design/ui.md#timers). It stops itself once the engine lands, since that transition happens once.\n const bootScope = effectScope(true);\n\n bootScope.run(() =>\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 queuedTimers = [ ...pendingTimers ];\n pendingTimers.clear();\n\n for(const start of queuedTimers)\n {\n start(eng);\n }\n\n const queuedCooldowns = [ ...pendingCooldowns ];\n pendingCooldowns.clear();\n\n for(const start of queuedCooldowns)\n {\n start(eng);\n }\n\n bootScope.stop();\n }, { immediate: true, flush: 'sync' });\n });\n\n onUnmounted(() =>\n {\n pendingTimers.clear();\n\n for(const cancel of cancellers)\n {\n cancel();\n }\n\n cancellers.length = 0;\n\n if(pendingCooldowns.size === 0)\n {\n bootScope.stop();\n }\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 : ObservedEngine) => () => void) : () => void\n {\n let cancel : (() => void) | null = null;\n\n const begin = (eng : ObservedEngine) : 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 pendingTimers.add(begin);\n }\n\n return () =>\n {\n pendingTimers.delete(begin);\n\n if(cancel)\n {\n release(cancel);\n cancel = null;\n }\n };\n }\n\n // Refused at the call, so a duration that is wrong is wrong now rather than when the engine lands\n // (design/ui.md#timers).\n function delay(ms : number, callback : () => void) : () => void\n {\n if(ms < 0)\n {\n throw new RangeError(`delay() takes zero or more milliseconds of game time, and was given ${ ms }.`);\n }\n\n return schedule((eng) => eng.timer.delay(ms, callback));\n }\n\n function interval(ms : number, callback : () => void) : () => void\n {\n if(ms <= 0)\n {\n throw new RangeError(\n `interval() takes a positive number of milliseconds of game time, and was given ${ ms }.`\n );\n }\n\n return schedule((eng) => eng.timer.interval(ms, callback));\n }\n\n /**\n * Cooldowns are passive pollable objects with nothing to cancel, so unmounting leaves a queued one waiting\n * rather than dropping it (design/ui.md#timers). Before the engine boots the caller gets a stand-in that\n * forwards to the real handle once there is one; a `reset()` landing 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 pendingCooldowns.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 the operations both composables share\n// (design/ui.md#how-a-view-notices-a-change).\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { reactive } from 'vue';\nimport type { BehaviorClass, GameEntity, GameEntityType } from '@skewedaspect/sage';\n\n// Symbols\nimport type { ObservedSimulation } from '../symbols.ts';\n\n//----------------------------------------------------------------------------------------------------------------------\n\ntype Bag = Record<string, unknown>;\n\n/** The bag a view holds its state in: reactive for a template, and with no prototype the way its records are. */\nexport function createStateBag() : Bag\n{\n return reactive(Object.create(null) as Bag);\n}\n\n/**\n * State is finite JSON-compatible data, and entity state copies records with no prototype\n * (design/entities.md#state), so a view's records carry none either and a field named `__proto__` or\n * `constructor` stays an ordinary field. The walk is hand-rolled because `structuredClone` cannot produce a\n * null-prototype copy.\n */\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 = Object.create(null) as 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 */\nexport function resolveEntity(\n simulation : ObservedSimulation | 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 */\nexport function resolveTagged(\n simulation : ObservedSimulation | 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 */\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// Awaiting a view reads `then`, and handing one to Vue reads these, so answering each with a function would\n// have the proxy invent an operation nothing declared.\nconst NOT_OPERATIONS = new Set([ 'then', '__v_isRef', '__v_isReactive', '__v_isReadonly', '__v_raw', '__v_skip' ]);\n\n/**\n * Builds the operations a view exposes. Every call resolves the entity again, so a view whose entity has gone\n * throws rather than reaching the one it used to hold (design/ui.md#absence). One name answers with one\n * function for the view's life, so a template handing an operation to a child hands it the same function on\n * every render (design/ui.md#gameentityview).\n */\nexport function buildOps(id : string, find : () => GameEntity | undefined) : Record<string, unknown>\n{\n const operations = new Map<string, (...args : unknown[]) => unknown>();\n\n return new Proxy({}, {\n get(_target : object, name : string | symbol) : unknown\n {\n if(typeof name === 'symbol' || NOT_OPERATIONS.has(name))\n {\n return undefined;\n }\n\n const existing = operations.get(name);\n\n if(existing !== undefined)\n {\n return existing;\n }\n\n const operation = (...args : unknown[]) : unknown =>\n {\n const entity = find();\n\n if(entity === undefined)\n {\n throw new Error(`No entity \"${ id }\" to call \"${ name }\" on.`);\n }\n\n const ops = entity.ops as Record<string, (...args : unknown[]) => unknown>;\n\n if(!Object.hasOwn(ops, name))\n {\n throw new Error(`Entity \"${ id }\" exposes no operation \"${ name }\".`);\n }\n\n return ops[name](...args);\n };\n\n operations.set(name, operation);\n\n return operation;\n },\n });\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEntity Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, onUnmounted, ref, shallowRef, 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, createStateBag, 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 `Ref` holding a view of one entity, resolved by class and ID against the simulation the\n * engine currently holds. The view observes as it is created and refreshes once per rendered frame, pause\n * included (design/ui.md#when-a-view-observes). The `Ref` never changes what it holds, since the class and\n * the ID are fixed for its life; what a refresh changes is inside the view (design/ui.md#gameentityview).\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) : Ref<GameEntityView<Type>>\n{\n const { engine } = useSageEngine();\n\n const state = createStateBag();\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 const view : GameEntityView<Type> = {\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 // shallowRef, so the Ref hands back the view itself: its state is already reactive, and a deep ref would\n // wrap the operation proxy as well.\n return shallowRef(view);\n}\n\n//----------------------------------------------------------------------------------------------------------------------\n","//----------------------------------------------------------------------------------------------------------------------\n// useSageEntities Composable\n//----------------------------------------------------------------------------------------------------------------------\n\nimport { type Ref, onUnmounted, 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, createStateBag, 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 // A view answers ops and hasBehavior from the last refresh's matched membership, not a fresh lookup by\n // ID, so a member that lost the tag or the narrowing behavior agrees with its own present\n // (design/ui.md#absence).\n const findEntity = (id : string) : GameEntity | undefined =>\n {\n if(!held.has(id))\n {\n return undefined;\n }\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 = createStateBag();\n const find = () : GameEntity | undefined => findEntity(id);\n const ops = buildOps(id, find);\n const hasBehavior = buildHasBehavior(find);\n\n // A view is present exactly while the entity it holds is one the last refresh resolved\n // (design/ui.md#absence).\n const view : GameEntityView = {\n id,\n get present() : boolean { return views.value.includes(view); },\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 const record : Held = { state, view };\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":"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"}
|
package/dist/symbols.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
import { InjectionKey,
|
|
2
|
-
import { GameEngine } from '@skewedaspect/sage';
|
|
3
|
-
|
|
1
|
+
import { InjectionKey, Ref } from 'vue';
|
|
2
|
+
import { GameEngine, GameManager, GameSimulationManager } from '@skewedaspect/sage';
|
|
3
|
+
/**
|
|
4
|
+
* What sage-vue reads of a simulation: its two entity lookups. GameSimulationManager is invariant in its registry
|
|
5
|
+
* type parameter, so naming only the members whose signature does not mention the registry is what lets a
|
|
6
|
+
* simulation built from any registry satisfy this (design/ui.md#reaching-the-engine).
|
|
7
|
+
*/
|
|
8
|
+
export type ObservedSimulation = Pick<GameSimulationManager, 'getEntity' | 'getEntitiesByTag'>;
|
|
9
|
+
/**
|
|
10
|
+
* What sage-vue reads of an engine: enough to hear the bus, run timers, look entities up, and refresh views
|
|
11
|
+
* once a frame. An engine typed by any entity registry is one of these (design/ui.md#reaching-the-engine).
|
|
12
|
+
*/
|
|
13
|
+
export type ObservedEngine = Pick<GameEngine, 'bus' | 'timer' | 'renderEngine' | 'stop'> & {
|
|
14
|
+
readonly simulation: ObservedSimulation | null;
|
|
15
|
+
readonly managers: {
|
|
16
|
+
readonly gameManager: Pick<GameManager, 'registerFrameCallback'>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export declare const SAGE_ENGINE_KEY: InjectionKey<Readonly<Ref<ObservedEngine | null>>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skewedaspect/sage-vue",
|
|
3
|
-
"version": "0.10.0-
|
|
3
|
+
"version": "0.10.0-rc.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/sage-vue.js",
|
|
6
6
|
"module": "dist/sage-vue.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "https://gitlab.com/skewed-aspect/sage/sage",
|
|
19
|
+
"url": "git+https://gitlab.com/skewed-aspect/sage/sage.git",
|
|
20
20
|
"directory": "packages/sage-vue"
|
|
21
21
|
},
|
|
22
22
|
"author": "Christopher S. Case<chris.case@g33xnexus.com>",
|
|
@@ -25,21 +25,24 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "vite build",
|
|
27
27
|
"test": "vitest run --config vitest.config.ts",
|
|
28
|
-
"lint:types": "vue-tsc --noEmit"
|
|
28
|
+
"lint:types": "vue-tsc --noEmit && vue-tsc --noEmit -p tests/tsconfig.json"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
33
36
|
"peerDependencies": {
|
|
34
|
-
"@skewedaspect/sage": "
|
|
37
|
+
"@skewedaspect/sage": "0.10.0-rc.2",
|
|
35
38
|
"vue": "^3.5.0"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@skewedaspect/sage": "
|
|
41
|
+
"@skewedaspect/sage": "0.10.0-rc.2",
|
|
39
42
|
"@vitejs/plugin-vue": "^6.0.3",
|
|
43
|
+
"typescript": "^6.0.2",
|
|
40
44
|
"vite": "^8.0.3",
|
|
41
45
|
"vite-plugin-dts": "^4.5.4",
|
|
42
|
-
"typescript": "^6.0.2",
|
|
43
46
|
"vue": "^3.5.27",
|
|
44
47
|
"vue-tsc": "^3.3.11"
|
|
45
48
|
}
|