@khorsheed/dsh-game 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.en.md +53 -0
- package/README.i18n.yaml +6 -0
- package/README.md +52 -2
- package/cordis.patch.yml +23 -0
- package/lib/client.js +30348 -0
- package/lib/index.js +4154 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3769 -0
- package/lib/typert.remote-client.d.ts +101 -0
- package/lib/typert.remote-client.js +2082 -0
- package/lib/types/agent.d.ts +20 -0
- package/lib/types/agent.js +42 -0
- package/lib/types/checks.d.ts +44 -0
- package/lib/types/checks.js +267 -0
- package/lib/types/client/DesignView.d.ts +22 -0
- package/lib/types/client/DesignView.js +134 -0
- package/lib/types/client/ElementsView.d.ts +21 -0
- package/lib/types/client/ElementsView.js +187 -0
- package/lib/types/client/GameTab.d.ts +14 -0
- package/lib/types/client/GameTab.js +256 -0
- package/lib/types/client/PlayFrame.d.ts +54 -0
- package/lib/types/client/PlayFrame.js +129 -0
- package/lib/types/client/PlayView.d.ts +19 -0
- package/lib/types/client/PlayView.js +161 -0
- package/lib/types/client/Preview3D.d.ts +50 -0
- package/lib/types/client/Preview3D.js +176 -0
- package/lib/types/client/ProjectSwitcher.d.ts +20 -0
- package/lib/types/client/ProjectSwitcher.js +75 -0
- package/lib/types/client/Recipe3DEditor.d.ts +19 -0
- package/lib/types/client/Recipe3DEditor.js +44 -0
- package/lib/types/client/StyleGallery.d.ts +18 -0
- package/lib/types/client/StyleGallery.js +27 -0
- package/lib/types/client/TurnCard.d.ts +62 -0
- package/lib/types/client/TurnCard.js +100 -0
- package/lib/types/client/VersionsView.d.ts +17 -0
- package/lib/types/client/VersionsView.js +68 -0
- package/lib/types/client/chat-node.d.ts +42 -0
- package/lib/types/client/chat-node.js +77 -0
- package/lib/types/client/contract.d.ts +125 -0
- package/lib/types/client/contract.js +6 -0
- package/lib/types/client/definition.d.ts +22 -0
- package/lib/types/client/definition.js +28 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/index.js +229 -0
- package/lib/types/client/locales.d.ts +376 -0
- package/lib/types/client/locales.js +727 -0
- package/lib/types/client/looks.d.ts +36 -0
- package/lib/types/client/looks.js +105 -0
- package/lib/types/client/markdown.d.ts +11 -0
- package/lib/types/client/markdown.js +52 -0
- package/lib/types/client/preset-visibility.d.ts +94 -0
- package/lib/types/client/preset-visibility.js +115 -0
- package/lib/types/client/preview-entry.d.ts +24 -0
- package/lib/types/client/preview-entry.js +107 -0
- package/lib/types/client/refs.d.ts +36 -0
- package/lib/types/client/refs.js +44 -0
- package/lib/types/client/style-samples.d.ts +5 -0
- package/lib/types/client/style-samples.js +196 -0
- package/lib/types/fsutil.d.ts +26 -0
- package/lib/types/fsutil.js +104 -0
- package/lib/types/hooks.d.ts +83 -0
- package/lib/types/hooks.js +108 -0
- package/lib/types/index.d.ts +30 -0
- package/lib/types/index.js +22 -0
- package/lib/types/kits/canvas2d.d.ts +6 -0
- package/lib/types/kits/canvas2d.js +60 -0
- package/lib/types/kits/ids.d.ts +11 -0
- package/lib/types/kits/ids.js +6 -0
- package/lib/types/kits/index.d.ts +26 -0
- package/lib/types/kits/index.js +28 -0
- package/lib/types/kits/three3d.d.ts +12 -0
- package/lib/types/kits/three3d.js +188 -0
- package/lib/types/kits/types.d.ts +52 -0
- package/lib/types/kits/types.js +6 -0
- package/lib/types/look3d.d.ts +20 -0
- package/lib/types/look3d.js +175 -0
- package/lib/types/panel-session.d.ts +21 -0
- package/lib/types/panel-session.js +16 -0
- package/lib/types/project.d.ts +132 -0
- package/lib/types/project.js +494 -0
- package/lib/types/remote.d.ts +182 -0
- package/lib/types/remote.js +256 -0
- package/lib/types/skills.d.ts +13 -0
- package/lib/types/skills.js +140 -0
- package/lib/types/store.d.ts +325 -0
- package/lib/types/store.js +1285 -0
- package/lib/types/tools.d.ts +12 -0
- package/lib/types/tools.js +301 -0
- package/lib/types/trash.d.ts +5 -0
- package/lib/types/trash.js +64 -0
- package/lib/types/types.d.ts +522 -0
- package/lib/types/types.js +85 -0
- package/lib/types/vfs.d.ts +92 -0
- package/lib/types/vfs.js +217 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +132 -3
- package/runtime/dsh3d.js +1279 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element looks in the panel: the same pixel rows the game draws through
|
|
3
|
+
* `dshDraw`, painted onto a small canvas. The canvas is the content here
|
|
4
|
+
* (the element's own colors), so no theme token applies to the pixels.
|
|
5
|
+
*/
|
|
6
|
+
import { type ReactNode } from 'react';
|
|
7
|
+
import { type ElementLook } from '../types.ts';
|
|
8
|
+
/** Rows of one state (the first state when the name is unknown). */
|
|
9
|
+
export declare function rowsOf(look: ElementLook, state: string): readonly string[];
|
|
10
|
+
/** Paint one state of a look into a 2D context, cell by cell. */
|
|
11
|
+
export declare function paintLook(g: CanvasRenderingContext2D, look: ElementLook, state: string, cell: number, grid?: boolean): void;
|
|
12
|
+
/** Props of {@link LookCanvas}. */
|
|
13
|
+
export interface LookCanvasProps {
|
|
14
|
+
readonly look: ElementLook;
|
|
15
|
+
readonly state?: string;
|
|
16
|
+
/** Box edge in CSS px; the look scales to the largest whole cell that fits. */
|
|
17
|
+
readonly box: number;
|
|
18
|
+
readonly grid?: boolean;
|
|
19
|
+
readonly className?: string | undefined;
|
|
20
|
+
/** Editor mode: called with the cell under the pointer while painting. */
|
|
21
|
+
readonly onPaint?: (x: number, y: number) => void;
|
|
22
|
+
}
|
|
23
|
+
/** One look on a checkerboard, crisp at any size. */
|
|
24
|
+
export declare function LookCanvas({ look, state, box, grid, className, onPaint }: LookCanvasProps): ReactNode;
|
|
25
|
+
/** Replace one cell of one state; returns a new look. */
|
|
26
|
+
export declare function setCell(look: ElementLook, state: string, x: number, y: number, char: string): ElementLook;
|
|
27
|
+
/** Swap one palette color for another everywhere (整体换色). */
|
|
28
|
+
export declare function recolor(look: ElementLook, index: number, color: string): ElementLook;
|
|
29
|
+
/** The palette index for a color, appending it when new; returns the new look and the row char. */
|
|
30
|
+
export declare function ensureColor(look: ElementLook, color: string): {
|
|
31
|
+
look: ElementLook;
|
|
32
|
+
char: string;
|
|
33
|
+
};
|
|
34
|
+
/** A blank 16×16 look in a palette, for elements that have none yet. */
|
|
35
|
+
export declare function blankLook(palette: readonly string[]): ElementLook;
|
|
36
|
+
//# sourceMappingURL=looks.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Element looks in the panel: the same pixel rows the game draws through
|
|
4
|
+
* `dshDraw`, painted onto a small canvas. The canvas is the content here
|
|
5
|
+
* (the element's own colors), so no theme token applies to the pixels.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useRef } from 'react';
|
|
8
|
+
import { LOOK_CHARS } from "../types.js";
|
|
9
|
+
import css from './GameTab.module.css';
|
|
10
|
+
/** Rows of one state (the first state when the name is unknown). */
|
|
11
|
+
export function rowsOf(look, state) {
|
|
12
|
+
return look.states[state] ?? Object.values(look.states)[0] ?? [];
|
|
13
|
+
}
|
|
14
|
+
/** Paint one state of a look into a 2D context, cell by cell. */
|
|
15
|
+
export function paintLook(g, look, state, cell, grid = false) {
|
|
16
|
+
const rows = rowsOf(look, state);
|
|
17
|
+
const w = rows[0]?.length ?? 0;
|
|
18
|
+
g.clearRect(0, 0, w * cell, rows.length * cell);
|
|
19
|
+
rows.forEach((row, y) => {
|
|
20
|
+
for (let x = 0; x < row.length; x++) {
|
|
21
|
+
const color = look.palette[LOOK_CHARS.indexOf(row[x])];
|
|
22
|
+
if (color === undefined)
|
|
23
|
+
continue;
|
|
24
|
+
g.fillStyle = color;
|
|
25
|
+
g.fillRect(x * cell, y * cell, cell, cell);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (grid && cell >= 6) {
|
|
29
|
+
g.strokeStyle = 'rgba(127,127,127,0.18)'; /* panel-check: allow-color — editor grid over the element's own pixels */
|
|
30
|
+
g.lineWidth = 1;
|
|
31
|
+
for (let x = 0; x <= w; x++) {
|
|
32
|
+
g.beginPath();
|
|
33
|
+
g.moveTo(x * cell + 0.5, 0);
|
|
34
|
+
g.lineTo(x * cell + 0.5, rows.length * cell);
|
|
35
|
+
g.stroke();
|
|
36
|
+
}
|
|
37
|
+
for (let y = 0; y <= rows.length; y++) {
|
|
38
|
+
g.beginPath();
|
|
39
|
+
g.moveTo(0, y * cell + 0.5);
|
|
40
|
+
g.lineTo(w * cell, y * cell + 0.5);
|
|
41
|
+
g.stroke();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** One look on a checkerboard, crisp at any size. */
|
|
46
|
+
export function LookCanvas({ look, state = 'idle', box, grid = false, className, onPaint }) {
|
|
47
|
+
const ref = useRef(null);
|
|
48
|
+
const rows = rowsOf(look, state);
|
|
49
|
+
const w = Math.max(1, rows[0]?.length ?? 1);
|
|
50
|
+
const h = Math.max(1, rows.length);
|
|
51
|
+
const cell = Math.max(1, Math.floor(box / Math.max(w, h)));
|
|
52
|
+
const painting = useRef(false);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const g = ref.current?.getContext('2d');
|
|
55
|
+
if (g)
|
|
56
|
+
paintLook(g, look, state, cell, grid);
|
|
57
|
+
}, [look, state, cell, grid]);
|
|
58
|
+
const cellAt = (event) => {
|
|
59
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
60
|
+
const x = Math.floor((event.clientX - rect.left) / rect.width * w);
|
|
61
|
+
const y = Math.floor((event.clientY - rect.top) / rect.height * h);
|
|
62
|
+
return x >= 0 && y >= 0 && x < w && y < h ? [x, y] : undefined;
|
|
63
|
+
};
|
|
64
|
+
const paint = (event) => {
|
|
65
|
+
const at = cellAt(event);
|
|
66
|
+
if (at && onPaint)
|
|
67
|
+
onPaint(at[0], at[1]);
|
|
68
|
+
};
|
|
69
|
+
return _jsx("canvas", { ref: ref, className: [css.look, className].filter(Boolean).join(' '), width: w * cell, height: h * cell, "data-editable": onPaint ? 'true' : 'false', ...(onPaint ? {
|
|
70
|
+
onPointerDown: (e) => { painting.current = true; e.currentTarget.setPointerCapture(e.pointerId); paint(e); },
|
|
71
|
+
onPointerMove: (e) => { if (painting.current)
|
|
72
|
+
paint(e); },
|
|
73
|
+
onPointerUp: () => { painting.current = false; },
|
|
74
|
+
onPointerCancel: () => { painting.current = false; },
|
|
75
|
+
} : {}) });
|
|
76
|
+
}
|
|
77
|
+
/** Replace one cell of one state; returns a new look. */
|
|
78
|
+
export function setCell(look, state, x, y, char) {
|
|
79
|
+
const name = look.states[state] ? state : Object.keys(look.states)[0];
|
|
80
|
+
const rows = [...rowsOf(look, name)];
|
|
81
|
+
const row = rows[y];
|
|
82
|
+
if (row === undefined || row[x] === char)
|
|
83
|
+
return look;
|
|
84
|
+
rows[y] = row.slice(0, x) + char + row.slice(x + 1);
|
|
85
|
+
return { ...look, states: { ...look.states, [name]: rows } };
|
|
86
|
+
}
|
|
87
|
+
/** Swap one palette color for another everywhere (整体换色). */
|
|
88
|
+
export function recolor(look, index, color) {
|
|
89
|
+
if (index < 0 || index >= look.palette.length)
|
|
90
|
+
return look;
|
|
91
|
+
return { ...look, palette: look.palette.map((c, i) => (i === index ? color : c)) };
|
|
92
|
+
}
|
|
93
|
+
/** The palette index for a color, appending it when new; returns the new look and the row char. */
|
|
94
|
+
export function ensureColor(look, color) {
|
|
95
|
+
const at = look.palette.findIndex(c => c.toLowerCase() === color.toLowerCase());
|
|
96
|
+
if (at >= 0)
|
|
97
|
+
return { look, char: LOOK_CHARS[at] };
|
|
98
|
+
if (look.palette.length >= LOOK_CHARS.length)
|
|
99
|
+
return { look, char: LOOK_CHARS[0] };
|
|
100
|
+
return { look: { ...look, palette: [...look.palette, color] }, char: LOOK_CHARS[look.palette.length] };
|
|
101
|
+
}
|
|
102
|
+
/** A blank 16×16 look in a palette, for elements that have none yet. */
|
|
103
|
+
export function blankLook(palette) {
|
|
104
|
+
return { palette: palette.slice(0, 16), states: { idle: Array.from({ length: 16 }, () => '.'.repeat(16)) } };
|
|
105
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deliberately small markdown view for design chapters: headings, lists,
|
|
3
|
+
* quotes, pipe tables and paragraphs. No HTML passes through — every line is
|
|
4
|
+
* rendered as text.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
/** Render chapter text. */
|
|
8
|
+
export declare function Markdown({ text }: {
|
|
9
|
+
text: string;
|
|
10
|
+
}): ReactNode;
|
|
11
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import css from './GameTab.module.css';
|
|
3
|
+
/** Split a pipe-table row into its cells. */
|
|
4
|
+
function cells(line) {
|
|
5
|
+
return line.trim().replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
6
|
+
}
|
|
7
|
+
/** Render chapter text. */
|
|
8
|
+
export function Markdown({ text }) {
|
|
9
|
+
const lines = text.split('\n');
|
|
10
|
+
const out = [];
|
|
11
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12
|
+
const line = lines[i] ?? '';
|
|
13
|
+
if (line.trim() === '')
|
|
14
|
+
continue;
|
|
15
|
+
const heading = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
16
|
+
if (heading !== null) {
|
|
17
|
+
out.push(heading[1].length === 1 ? _jsx("h3", { className: css.mdH1, children: heading[2] }, i) : _jsx("h4", { className: css.mdH2, children: heading[2] }, i));
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (/^\s*\|/.test(line)) {
|
|
21
|
+
const rows = [];
|
|
22
|
+
while (i < lines.length && /^\s*\|/.test(lines[i] ?? '')) {
|
|
23
|
+
const row = lines[i] ?? '';
|
|
24
|
+
if (!/^\s*\|[\s:|-]+\|\s*$/.test(row))
|
|
25
|
+
rows.push(cells(row));
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
i--;
|
|
29
|
+
const [head, ...body] = rows;
|
|
30
|
+
out.push(_jsx("div", { className: css.mdTableWrap, children: _jsxs("table", { className: css.mdTable, children: [head !== undefined && _jsx("thead", { children: _jsx("tr", { children: head.map((c, j) => _jsx("th", { children: c }, j)) }) }), _jsx("tbody", { children: body.map((r, k) => _jsx("tr", { children: r.map((c, j) => _jsx("td", { children: c }, j)) }, k)) })] }) }, i));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
|
|
34
|
+
const items = [];
|
|
35
|
+
const ordered = /^\s*\d+\./.test(line);
|
|
36
|
+
while (i < lines.length && /^\s*([-*]|\d+\.)\s+/.test(lines[i] ?? '')) {
|
|
37
|
+
items.push((lines[i] ?? '').replace(/^\s*([-*]|\d+\.)\s+/, ''));
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
i--;
|
|
41
|
+
const list = items.map((item, k) => _jsx("li", { children: item }, k));
|
|
42
|
+
out.push(ordered ? _jsx("ol", { className: css.mdList, children: list }, i) : _jsx("ul", { className: css.mdList, children: list }, i));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith('>')) {
|
|
46
|
+
out.push(_jsx("blockquote", { className: css.mdQuote, children: line.replace(/^>\s?/, '') }, i));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
out.push(_jsx("p", { className: css.mdP, children: line }, i));
|
|
50
|
+
}
|
|
51
|
+
return _jsx("div", { className: css.md, children: out });
|
|
52
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preset-composition visibility of the game right-Sidebar tab type (the
|
|
3
|
+
* canvas dual check, docs/plugin-visibility.md). The tab renders the current
|
|
4
|
+
* session's bound project, so it shows exactly where the session reaches the
|
|
5
|
+
* game tools, by either grant path of the `./agent` entry:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Profile-root mount** — the inventory's `entries` carry an ENABLED
|
|
8
|
+
* `@khorsheed/dsh-game/agent` row → visible for every session.
|
|
9
|
+
* 2. **Preset mount** (the shipped shape: the root row disabled, the game
|
|
10
|
+
* preset names the entry) → visible exactly when the CURRENT session's
|
|
11
|
+
* preset composition names that row.
|
|
12
|
+
*
|
|
13
|
+
* Every unreadable path fails OPEN (visible): no pluginInventory namespace, a
|
|
14
|
+
* pending/failed RPC, a missing or `broken` preset group, a session with no
|
|
15
|
+
* preset, and the no-session home state. Hidden means NOT registered (the
|
|
16
|
+
* guide enumerates the tab-type registry; an opened tab of an unregistered
|
|
17
|
+
* kind renders the host's `tab.unavailable` fallback).
|
|
18
|
+
* @module @khorsheed/dsh-game/client/preset-visibility
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
22
|
+
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
|
|
23
|
+
/** The criterion's row: the game tools/prompt composition entry. */
|
|
24
|
+
export declare const GAME_AGENT_ROW_MODULE = "@khorsheed/dsh-game/agent";
|
|
25
|
+
/**
|
|
26
|
+
* Minimal structural mirror of the official pluginInventory snapshot — the
|
|
27
|
+
* entries slice (root grants) plus the agentPresets slice (preset grants),
|
|
28
|
+
* duck-typed and host-version tolerant.
|
|
29
|
+
*/
|
|
30
|
+
export interface GamePluginInventorySnapshot {
|
|
31
|
+
readonly entries?: readonly {
|
|
32
|
+
readonly moduleName: string;
|
|
33
|
+
readonly enabled?: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
readonly agentPresets?: readonly {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly broken?: string;
|
|
38
|
+
readonly rows: readonly {
|
|
39
|
+
readonly moduleName: string;
|
|
40
|
+
readonly enabled?: boolean;
|
|
41
|
+
}[];
|
|
42
|
+
}[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The tab's visibility controller: one inventory fetch per page (composition
|
|
46
|
+
* data changes only with preset files, which a reload re-reads anyway),
|
|
47
|
+
* re-evaluated by the toggle on every session switch.
|
|
48
|
+
*/
|
|
49
|
+
export declare class GameTabVisibility {
|
|
50
|
+
private readonly ctx;
|
|
51
|
+
/** The inventory answer; null while pending/failed and on namespace-less hosts. */
|
|
52
|
+
private composition;
|
|
53
|
+
private readonly listeners;
|
|
54
|
+
/**
|
|
55
|
+
* @param ctx - client root context.
|
|
56
|
+
* @param pluginInventory - the OFFICIAL namespace, probed through ctx.get
|
|
57
|
+
* (never injected — declaring it would pend the whole client on a host
|
|
58
|
+
* without it, and the criterion must fail open there).
|
|
59
|
+
*/
|
|
60
|
+
constructor(ctx: Context, pluginInventory: {
|
|
61
|
+
list: () => Promise<RemoteResult<GamePluginInventorySnapshot>>;
|
|
62
|
+
} | undefined);
|
|
63
|
+
show(sessionId: SessionId | undefined): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the session reaches any of `modules` by either grant path — the
|
|
66
|
+
* tab's criterion over the game agent row. Every unreadable path fails open, as above.
|
|
67
|
+
*/
|
|
68
|
+
grantsAny(sessionId: SessionId | undefined, modules: readonly string[]): boolean;
|
|
69
|
+
/** Subscribe to criterion-input changes (session switches, the inventory answer). */
|
|
70
|
+
subscribe(listener: () => void): () => void;
|
|
71
|
+
private notify;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The tab type's registration toggle (the room template): registers exactly
|
|
75
|
+
* while the criterion passes for the current session and disposes otherwise.
|
|
76
|
+
* Unlike the keyed-slot consumers, the sidebarRightTabs registry is a direct
|
|
77
|
+
* inject — no slot-declaration wait, so the toggle is ready immediately.
|
|
78
|
+
*/
|
|
79
|
+
export declare class RegistrationToggle {
|
|
80
|
+
private readonly register;
|
|
81
|
+
private readonly show;
|
|
82
|
+
private dispose;
|
|
83
|
+
private ready;
|
|
84
|
+
/**
|
|
85
|
+
* @param register - mounts the entry and returns its disposer.
|
|
86
|
+
* @param show - the current decision (re-read on every sync).
|
|
87
|
+
*/
|
|
88
|
+
constructor(register: () => () => void, show: () => boolean);
|
|
89
|
+
/** The registry is reachable — decisions may register now. */
|
|
90
|
+
setReady(ready: boolean): void;
|
|
91
|
+
/** Re-evaluate: register while shown and ready, dispose otherwise. */
|
|
92
|
+
sync(): void;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=preset-visibility.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** The criterion's row: the game tools/prompt composition entry. */
|
|
2
|
+
export const GAME_AGENT_ROW_MODULE = '@khorsheed/dsh-game/agent';
|
|
3
|
+
/** The current session's agent preset, the dual read per host line. */
|
|
4
|
+
function presetOf(row) {
|
|
5
|
+
const value = row
|
|
6
|
+
?.projectionValues?.agentPreset
|
|
7
|
+
?? row?.agentPreset;
|
|
8
|
+
return typeof value === 'string' ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The tab's visibility controller: one inventory fetch per page (composition
|
|
12
|
+
* data changes only with preset files, which a reload re-reads anyway),
|
|
13
|
+
* re-evaluated by the toggle on every session switch.
|
|
14
|
+
*/
|
|
15
|
+
export class GameTabVisibility {
|
|
16
|
+
ctx;
|
|
17
|
+
/** The inventory answer; null while pending/failed and on namespace-less hosts. */
|
|
18
|
+
composition = null;
|
|
19
|
+
listeners = new Set();
|
|
20
|
+
/**
|
|
21
|
+
* @param ctx - client root context.
|
|
22
|
+
* @param pluginInventory - the OFFICIAL namespace, probed through ctx.get
|
|
23
|
+
* (never injected — declaring it would pend the whole client on a host
|
|
24
|
+
* without it, and the criterion must fail open there).
|
|
25
|
+
*/
|
|
26
|
+
constructor(ctx, pluginInventory) {
|
|
27
|
+
this.ctx = ctx;
|
|
28
|
+
if (pluginInventory !== undefined) {
|
|
29
|
+
void pluginInventory.list().then(result => {
|
|
30
|
+
if (!result.ok)
|
|
31
|
+
return;
|
|
32
|
+
this.composition = result.value ?? null;
|
|
33
|
+
this.notify();
|
|
34
|
+
}).catch(() => { });
|
|
35
|
+
}
|
|
36
|
+
// Session switches re-decide the tab type's registration (the guide
|
|
37
|
+
// enumerates registrations, not components).
|
|
38
|
+
ctx.effect(() => ctx.sessions.list.subscribe(() => { this.notify(); }), 'game: tab visibility session follower');
|
|
39
|
+
}
|
|
40
|
+
show(sessionId) {
|
|
41
|
+
return this.grantsAny(sessionId, [GAME_AGENT_ROW_MODULE]);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether the session reaches any of `modules` by either grant path — the
|
|
45
|
+
* tab's criterion over the game agent row. Every unreadable path fails open, as above.
|
|
46
|
+
*/
|
|
47
|
+
grantsAny(sessionId, modules) {
|
|
48
|
+
if (this.composition === null)
|
|
49
|
+
return true;
|
|
50
|
+
// Path 1: a root-mounted agent row grants the tools to every session —
|
|
51
|
+
// a deployment-level constant, checked before any per-session question.
|
|
52
|
+
if (this.composition.entries?.some(row => modules.includes(row.moduleName) && row.enabled === true) === true)
|
|
53
|
+
return true;
|
|
54
|
+
if (sessionId === undefined)
|
|
55
|
+
return true;
|
|
56
|
+
const preset = presetOf(this.ctx.sessions.list.getSnapshot().byId[sessionId]);
|
|
57
|
+
if (preset === undefined)
|
|
58
|
+
return true;
|
|
59
|
+
const group = this.composition.agentPresets?.find(candidate => candidate.id === preset);
|
|
60
|
+
if (group === undefined || group.broken !== undefined)
|
|
61
|
+
return true;
|
|
62
|
+
// Path 2: the preset-mounted row grants this session.
|
|
63
|
+
return group.rows.some(row => modules.includes(row.moduleName) && row.enabled !== false);
|
|
64
|
+
}
|
|
65
|
+
/** Subscribe to criterion-input changes (session switches, the inventory answer). */
|
|
66
|
+
subscribe(listener) {
|
|
67
|
+
this.listeners.add(listener);
|
|
68
|
+
return () => { this.listeners.delete(listener); };
|
|
69
|
+
}
|
|
70
|
+
notify() {
|
|
71
|
+
for (const listener of this.listeners)
|
|
72
|
+
listener();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The tab type's registration toggle (the room template): registers exactly
|
|
77
|
+
* while the criterion passes for the current session and disposes otherwise.
|
|
78
|
+
* Unlike the keyed-slot consumers, the sidebarRightTabs registry is a direct
|
|
79
|
+
* inject — no slot-declaration wait, so the toggle is ready immediately.
|
|
80
|
+
*/
|
|
81
|
+
export class RegistrationToggle {
|
|
82
|
+
register;
|
|
83
|
+
show;
|
|
84
|
+
dispose;
|
|
85
|
+
ready = false;
|
|
86
|
+
/**
|
|
87
|
+
* @param register - mounts the entry and returns its disposer.
|
|
88
|
+
* @param show - the current decision (re-read on every sync).
|
|
89
|
+
*/
|
|
90
|
+
constructor(register, show) {
|
|
91
|
+
this.register = register;
|
|
92
|
+
this.show = show;
|
|
93
|
+
}
|
|
94
|
+
/** The registry is reachable — decisions may register now. */
|
|
95
|
+
setReady(ready) {
|
|
96
|
+
this.ready = ready;
|
|
97
|
+
if (!ready && this.dispose !== undefined) {
|
|
98
|
+
this.dispose();
|
|
99
|
+
this.dispose = undefined;
|
|
100
|
+
}
|
|
101
|
+
this.sync();
|
|
102
|
+
}
|
|
103
|
+
/** Re-evaluate: register while shown and ready, dispose otherwise. */
|
|
104
|
+
sync() {
|
|
105
|
+
if (!this.ready)
|
|
106
|
+
return;
|
|
107
|
+
if (this.show()) {
|
|
108
|
+
this.dispose ??= this.register();
|
|
109
|
+
}
|
|
110
|
+
else if (this.dispose !== undefined) {
|
|
111
|
+
this.dispose();
|
|
112
|
+
this.dispose = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 3D preview's in-frame entry (the element turntable, card stills,
|
|
3
|
+
* proposal stills): plain module source, run in the same sandbox as the play
|
|
4
|
+
* area on the live bundle. Kept apart from the React component so a spec can
|
|
5
|
+
* run it through the real checker.
|
|
6
|
+
*
|
|
7
|
+
* The camera is a perspective orbit around the model. Models face +x (the
|
|
8
|
+
* kit's convention, built-in bodies included), so yaw 0 looks at the face,
|
|
9
|
+
* 90 from the side, 180 from behind; pitch looks down from above. The default
|
|
10
|
+
* view is the look's `preview` (`{ yaw, pitch }` in degrees), else a
|
|
11
|
+
* three-quarter front view from slightly above. A turntable drifts until it
|
|
12
|
+
* is dragged (sideways turns, up and down tilts) or told which side to show.
|
|
13
|
+
* @module @khorsheed/dsh-game/client
|
|
14
|
+
*/
|
|
15
|
+
/** The id the previewed look rides under in `window.dshElements`. */
|
|
16
|
+
export declare const PREVIEW_ID = "__preview";
|
|
17
|
+
/** The default view: three-quarter front, a little from above. */
|
|
18
|
+
export declare const PREVIEW_YAW = 35;
|
|
19
|
+
export declare const PREVIEW_PITCH = 18;
|
|
20
|
+
/** The preview entry module (it runs in the frame). */
|
|
21
|
+
export declare const PREVIEW_MAIN = "\nconst { createWorld, build, THREE } = await import('dsh3d')\ntry { await import('../src/looks.js') } catch (e) {}\nconst mode = (await (await fetch('_preview/mode.json')).json()).mode\nconst world = createWorld({ canvas: document.querySelector('canvas') })\nconst cam = new THREE.PerspectiveCamera(32, 1, 0.05, 500)\nworld.setCamera(cam)\nconst deg = Math.PI / 180\nlet model = null, yaw = 35, pitch = 18, radius = 4, focusY = 0.5, drag = null, spinning = mode === 'spin', viewKey = null\nconst lookNow = () => window.dshElements['__preview'].look\n/** The look's own preview angle, taken again only when it changes (an edit must not reset a dragged view). */\nfunction viewFrom(look) {\n const p = (look && look.preview) || {}, key = JSON.stringify(p)\n if (key === viewKey) return\n viewKey = key\n yaw = Number.isFinite(p.yaw) ? p.yaw : 35\n pitch = Number.isFinite(p.pitch) ? p.pitch : 18\n}\nfunction aim() {\n pitch = Math.max(-10, Math.min(80, pitch))\n const p = pitch * deg, y = yaw * deg\n cam.position.set(Math.cos(p) * Math.cos(y) * radius, focusY + Math.sin(p) * radius, Math.cos(p) * Math.sin(y) * radius)\n cam.lookAt(0, focusY, 0)\n}\n/** Stand the model on the origin and back the camera off far enough to frame it. */\nfunction place() {\n model.position.set(0, 0, 0)\n model.updateMatrixWorld(true)\n const box = new THREE.Box3().setFromObject(model)\n if (box.isEmpty()) return\n const center = box.getCenter(new THREE.Vector3()), size = box.getSize(new THREE.Vector3())\n model.position.set(-center.x, -box.min.y, -center.z)\n focusY = size.y / 2\n const r = Math.max(0.3, box.getBoundingSphere(new THREE.Sphere()).radius)\n radius = r / Math.sin(cam.fov * deg / 2) * 1.08\n aim()\n}\nasync function fit() {\n if (model) world.remove(model)\n const look = lookNow()\n viewFrom(look)\n // From the look object, not the id: the world's own rebuild-by-element must not touch the preview.\n model = build(look)\n world.add(model, { id: '__preview' })\n place()\n // A glTF look fills in later: measure again once it has, then say what it can be tuned by.\n if (model.userData.loaded) { await model.userData.loaded; place() }\n parent.postMessage({ gamedev: 'preview-clips', names: model.userData.animations ?? [], parts: model.userData.parts?.() ?? null }, '*')\n}\n/** What a rebuild depends on; anything else (tint, bones, morphs, angle, clip) is retuned in place. */\nconst rebuildKey = look => JSON.stringify(look && look.kind === 'gltf' ? [look.src, look.material, look.outline, look.scale] : look)\nlet builtKey = null\nfunction edited() {\n const look = lookNow(), key = rebuildKey(look)\n if (model && key === builtKey && model.userData.retune) { model.userData.retune(look); viewFrom(look); return }\n builtKey = key\n fit()\n}\naddEventListener('message', e => {\n const d = e.data\n if (!d || !model) return\n if (d.gamedev === 'preview-play') model.userData.play?.(d.name)\n if (d.gamedev === 'preview-view') { yaw = d.yaw; if (Number.isFinite(d.pitch)) pitch = d.pitch; spinning = false; aim(); world.render() }\n})\nbuiltKey = rebuildKey(lookNow())\nawait fit()\naddEventListener('dshelements', () => { setTimeout(edited) })\nconst canvas = world.canvas\ncanvas.addEventListener('pointerdown', e => { drag = { x: e.clientX, y: e.clientY }; spinning = false; canvas.setPointerCapture(e.pointerId) })\ncanvas.addEventListener('pointermove', e => {\n if (drag === null) return\n yaw -= (e.clientX - drag.x) * 0.5\n pitch += (e.clientY - drag.y) * 0.35\n drag = { x: e.clientX, y: e.clientY }\n})\ncanvas.addEventListener('pointerup', () => { drag = null })\nworld.onUpdate(dt => { if (spinning && drag === null) yaw += dt * 18; aim() })\n// The checks (and specs) can drive this page like a game.\nwindow.dshGame = { ready: true, step: dt => world.step(dt), input() {}, state: () => ({ yaw, pitch, radius, focusY, perspective: world.camera === cam, spinning }) }\nif (mode === 'still') {\n world.step(0.4)\n parent.postMessage({ gamedev: 'preview', png: canvas.toDataURL('image/png') }, '*')\n} else world.start()\n";
|
|
22
|
+
/** The preview page: a full-size canvas and the entry above. */
|
|
23
|
+
export declare const PREVIEW_HTML: string;
|
|
24
|
+
//# sourceMappingURL=preview-entry.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 3D preview's in-frame entry (the element turntable, card stills,
|
|
3
|
+
* proposal stills): plain module source, run in the same sandbox as the play
|
|
4
|
+
* area on the live bundle. Kept apart from the React component so a spec can
|
|
5
|
+
* run it through the real checker.
|
|
6
|
+
*
|
|
7
|
+
* The camera is a perspective orbit around the model. Models face +x (the
|
|
8
|
+
* kit's convention, built-in bodies included), so yaw 0 looks at the face,
|
|
9
|
+
* 90 from the side, 180 from behind; pitch looks down from above. The default
|
|
10
|
+
* view is the look's `preview` (`{ yaw, pitch }` in degrees), else a
|
|
11
|
+
* three-quarter front view from slightly above. A turntable drifts until it
|
|
12
|
+
* is dragged (sideways turns, up and down tilts) or told which side to show.
|
|
13
|
+
* @module @khorsheed/dsh-game/client
|
|
14
|
+
*/
|
|
15
|
+
/** The id the previewed look rides under in `window.dshElements`. */
|
|
16
|
+
export const PREVIEW_ID = '__preview';
|
|
17
|
+
/** The default view: three-quarter front, a little from above. */
|
|
18
|
+
export const PREVIEW_YAW = 35;
|
|
19
|
+
export const PREVIEW_PITCH = 18;
|
|
20
|
+
/** The preview entry module (it runs in the frame). */
|
|
21
|
+
export const PREVIEW_MAIN = `
|
|
22
|
+
const { createWorld, build, THREE } = await import('dsh3d')
|
|
23
|
+
try { await import('../src/looks.js') } catch (e) {}
|
|
24
|
+
const mode = (await (await fetch('_preview/mode.json')).json()).mode
|
|
25
|
+
const world = createWorld({ canvas: document.querySelector('canvas') })
|
|
26
|
+
const cam = new THREE.PerspectiveCamera(32, 1, 0.05, 500)
|
|
27
|
+
world.setCamera(cam)
|
|
28
|
+
const deg = Math.PI / 180
|
|
29
|
+
let model = null, yaw = ${PREVIEW_YAW}, pitch = ${PREVIEW_PITCH}, radius = 4, focusY = 0.5, drag = null, spinning = mode === 'spin', viewKey = null
|
|
30
|
+
const lookNow = () => window.dshElements['${PREVIEW_ID}'].look
|
|
31
|
+
/** The look's own preview angle, taken again only when it changes (an edit must not reset a dragged view). */
|
|
32
|
+
function viewFrom(look) {
|
|
33
|
+
const p = (look && look.preview) || {}, key = JSON.stringify(p)
|
|
34
|
+
if (key === viewKey) return
|
|
35
|
+
viewKey = key
|
|
36
|
+
yaw = Number.isFinite(p.yaw) ? p.yaw : ${PREVIEW_YAW}
|
|
37
|
+
pitch = Number.isFinite(p.pitch) ? p.pitch : ${PREVIEW_PITCH}
|
|
38
|
+
}
|
|
39
|
+
function aim() {
|
|
40
|
+
pitch = Math.max(-10, Math.min(80, pitch))
|
|
41
|
+
const p = pitch * deg, y = yaw * deg
|
|
42
|
+
cam.position.set(Math.cos(p) * Math.cos(y) * radius, focusY + Math.sin(p) * radius, Math.cos(p) * Math.sin(y) * radius)
|
|
43
|
+
cam.lookAt(0, focusY, 0)
|
|
44
|
+
}
|
|
45
|
+
/** Stand the model on the origin and back the camera off far enough to frame it. */
|
|
46
|
+
function place() {
|
|
47
|
+
model.position.set(0, 0, 0)
|
|
48
|
+
model.updateMatrixWorld(true)
|
|
49
|
+
const box = new THREE.Box3().setFromObject(model)
|
|
50
|
+
if (box.isEmpty()) return
|
|
51
|
+
const center = box.getCenter(new THREE.Vector3()), size = box.getSize(new THREE.Vector3())
|
|
52
|
+
model.position.set(-center.x, -box.min.y, -center.z)
|
|
53
|
+
focusY = size.y / 2
|
|
54
|
+
const r = Math.max(0.3, box.getBoundingSphere(new THREE.Sphere()).radius)
|
|
55
|
+
radius = r / Math.sin(cam.fov * deg / 2) * 1.08
|
|
56
|
+
aim()
|
|
57
|
+
}
|
|
58
|
+
async function fit() {
|
|
59
|
+
if (model) world.remove(model)
|
|
60
|
+
const look = lookNow()
|
|
61
|
+
viewFrom(look)
|
|
62
|
+
// From the look object, not the id: the world's own rebuild-by-element must not touch the preview.
|
|
63
|
+
model = build(look)
|
|
64
|
+
world.add(model, { id: '${PREVIEW_ID}' })
|
|
65
|
+
place()
|
|
66
|
+
// A glTF look fills in later: measure again once it has, then say what it can be tuned by.
|
|
67
|
+
if (model.userData.loaded) { await model.userData.loaded; place() }
|
|
68
|
+
parent.postMessage({ gamedev: 'preview-clips', names: model.userData.animations ?? [], parts: model.userData.parts?.() ?? null }, '*')
|
|
69
|
+
}
|
|
70
|
+
/** What a rebuild depends on; anything else (tint, bones, morphs, angle, clip) is retuned in place. */
|
|
71
|
+
const rebuildKey = look => JSON.stringify(look && look.kind === 'gltf' ? [look.src, look.material, look.outline, look.scale] : look)
|
|
72
|
+
let builtKey = null
|
|
73
|
+
function edited() {
|
|
74
|
+
const look = lookNow(), key = rebuildKey(look)
|
|
75
|
+
if (model && key === builtKey && model.userData.retune) { model.userData.retune(look); viewFrom(look); return }
|
|
76
|
+
builtKey = key
|
|
77
|
+
fit()
|
|
78
|
+
}
|
|
79
|
+
addEventListener('message', e => {
|
|
80
|
+
const d = e.data
|
|
81
|
+
if (!d || !model) return
|
|
82
|
+
if (d.gamedev === 'preview-play') model.userData.play?.(d.name)
|
|
83
|
+
if (d.gamedev === 'preview-view') { yaw = d.yaw; if (Number.isFinite(d.pitch)) pitch = d.pitch; spinning = false; aim(); world.render() }
|
|
84
|
+
})
|
|
85
|
+
builtKey = rebuildKey(lookNow())
|
|
86
|
+
await fit()
|
|
87
|
+
addEventListener('dshelements', () => { setTimeout(edited) })
|
|
88
|
+
const canvas = world.canvas
|
|
89
|
+
canvas.addEventListener('pointerdown', e => { drag = { x: e.clientX, y: e.clientY }; spinning = false; canvas.setPointerCapture(e.pointerId) })
|
|
90
|
+
canvas.addEventListener('pointermove', e => {
|
|
91
|
+
if (drag === null) return
|
|
92
|
+
yaw -= (e.clientX - drag.x) * 0.5
|
|
93
|
+
pitch += (e.clientY - drag.y) * 0.35
|
|
94
|
+
drag = { x: e.clientX, y: e.clientY }
|
|
95
|
+
})
|
|
96
|
+
canvas.addEventListener('pointerup', () => { drag = null })
|
|
97
|
+
world.onUpdate(dt => { if (spinning && drag === null) yaw += dt * 18; aim() })
|
|
98
|
+
// The checks (and specs) can drive this page like a game.
|
|
99
|
+
window.dshGame = { ready: true, step: dt => world.step(dt), input() {}, state: () => ({ yaw, pitch, radius, focusY, perspective: world.camera === cam, spinning }) }
|
|
100
|
+
if (mode === 'still') {
|
|
101
|
+
world.step(0.4)
|
|
102
|
+
parent.postMessage({ gamedev: 'preview', png: canvas.toDataURL('image/png') }, '*')
|
|
103
|
+
} else world.start()
|
|
104
|
+
`;
|
|
105
|
+
/** The preview page: a full-size canvas and the entry above. */
|
|
106
|
+
export const PREVIEW_HTML = '<style>html,body{margin:0;height:100%;overflow:hidden}canvas{width:100%;height:100%;display:block;touch-action:none}</style>'
|
|
107
|
+
+ '<canvas></canvas><script type="module" src="_preview/main.js"></script>';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game reference chips: what a pick in the play area (or an element card)
|
|
3
|
+
* puts into the composer. The chip shows a short label; the model receives
|
|
4
|
+
* the full reference text — element name and id, instance, position, region
|
|
5
|
+
* and state — so it knows exactly which thing the user means.
|
|
6
|
+
* @module @khorsheed/dsh-game/client
|
|
7
|
+
*/
|
|
8
|
+
import type { PickRef } from '../vfs.ts';
|
|
9
|
+
/** The trigger-source name the chip codec registers under. */
|
|
10
|
+
export declare const GAME_REF_SOURCE = "dsh-game-ref";
|
|
11
|
+
/** Encode a chip payload. */
|
|
12
|
+
export declare function encodeGameRef(ref: {
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly text: string;
|
|
15
|
+
}): string;
|
|
16
|
+
/** Decode a chip payload; undefined on corruption. */
|
|
17
|
+
export declare function decodeGameRef(raw: string): {
|
|
18
|
+
label: string;
|
|
19
|
+
text: string;
|
|
20
|
+
} | undefined;
|
|
21
|
+
/** The codec: the model receives the full text; a corrupt chip refuses the send (the host restores the draft). */
|
|
22
|
+
export declare const gameRefCodec: {
|
|
23
|
+
clipboardText: (raw: string) => string;
|
|
24
|
+
serialize: (raw: string) => Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A pick as a chip: the label the user sees, the text the model gets.
|
|
28
|
+
* @param ref - what the game's `pick` answered (a bare id becomes `{ id }`).
|
|
29
|
+
* @param name - the element's name, when the id names one.
|
|
30
|
+
* @param groundLabel - the localized word for the ground.
|
|
31
|
+
*/
|
|
32
|
+
export declare function pickChip(ref: PickRef, name: string | undefined, groundLabel: string): {
|
|
33
|
+
label: string;
|
|
34
|
+
text: string;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=refs.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The trigger-source name the chip codec registers under. */
|
|
2
|
+
export const GAME_REF_SOURCE = 'dsh-game-ref';
|
|
3
|
+
/** Encode a chip payload. */
|
|
4
|
+
export function encodeGameRef(ref) {
|
|
5
|
+
return JSON.stringify({ v: 1, label: ref.label, text: ref.text });
|
|
6
|
+
}
|
|
7
|
+
/** Decode a chip payload; undefined on corruption. */
|
|
8
|
+
export function decodeGameRef(raw) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
return typeof parsed.text === 'string' && typeof parsed.label === 'string' ? { label: parsed.label, text: parsed.text } : undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The codec: the model receives the full text; a corrupt chip refuses the send (the host restores the draft). */
|
|
18
|
+
export const gameRefCodec = {
|
|
19
|
+
clipboardText: (raw) => decodeGameRef(raw)?.text ?? raw,
|
|
20
|
+
serialize: async (raw) => {
|
|
21
|
+
const text = decodeGameRef(raw)?.text;
|
|
22
|
+
if (text === undefined)
|
|
23
|
+
throw new Error('@khorsheed/dsh-game: corrupt game reference chip');
|
|
24
|
+
return text;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const num = (n) => String(Math.round(n * 10) / 10);
|
|
28
|
+
/**
|
|
29
|
+
* A pick as a chip: the label the user sees, the text the model gets.
|
|
30
|
+
* @param ref - what the game's `pick` answered (a bare id becomes `{ id }`).
|
|
31
|
+
* @param name - the element's name, when the id names one.
|
|
32
|
+
* @param groundLabel - the localized word for the ground.
|
|
33
|
+
*/
|
|
34
|
+
export function pickChip(ref, name, groundLabel) {
|
|
35
|
+
const where = ref.point === undefined ? '' : `(${num(ref.point.x)}, ${num(ref.point.z)})`;
|
|
36
|
+
if (ref.ground === true || ref.id === null) {
|
|
37
|
+
const label = [groundLabel, ref.region ?? where].filter(Boolean).join(' · ');
|
|
38
|
+
return { label, text: `〔${[groundLabel, ref.region, where].filter(Boolean).join(' · ')}〕` };
|
|
39
|
+
}
|
|
40
|
+
const title = name ?? ref.name ?? ref.id;
|
|
41
|
+
const label = [title, ref.state].filter(Boolean).join(' · ');
|
|
42
|
+
const idPart = ref.inst !== undefined && ref.inst !== null && ref.inst !== ref.id ? `${ref.id} #${ref.inst}` : ref.id;
|
|
43
|
+
return { label, text: `〔${[`${title}(${idPart})`, [ref.region, where].filter(Boolean).join(' '), ref.state].filter(Boolean).join(' · ')}〕` };
|
|
44
|
+
}
|