@energy8platform/game-engine 0.24.0 → 0.26.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/dist/devtools.cjs.js +593 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.d.ts +430 -0
- package/dist/devtools.esm.js +580 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/harness.cjs.js +34 -0
- package/dist/harness.cjs.js.map +1 -0
- package/dist/harness.d.ts +34 -0
- package/dist/harness.esm.js +31 -0
- package/dist/harness.esm.js.map +1 -0
- package/dist/reel-panel-client.cjs.js +622 -0
- package/dist/reel-panel-client.cjs.js.map +1 -0
- package/dist/reel-panel-client.d.ts +5 -0
- package/dist/reel-panel-client.esm.js +620 -0
- package/dist/reel-panel-client.esm.js.map +1 -0
- package/package.json +16 -1
- package/src/harness/index.ts +43 -0
- package/src/slot/devtools/configDiff.ts +41 -0
- package/src/slot/devtools/controlPanel.ts +151 -0
- package/src/slot/devtools/fieldSchema.ts +211 -0
- package/src/slot/devtools/index.ts +31 -0
- package/src/slot/devtools/panelClient.ts +126 -0
- package/src/slot/devtools/protocol.ts +29 -0
- package/src/slot/devtools/reelDevBridge.ts +80 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/devtools/panelClient.ts
|
|
2
|
+
//
|
|
3
|
+
// The harness "Reels" sidebar panel client. Built as a SELF-CONTAINED browser ESM
|
|
4
|
+
// (dist/reel-panel-client.js) and served by the harness at /__harness/panel/reels.js.
|
|
5
|
+
// Default-exports the HarnessPanelMount the core client calls.
|
|
6
|
+
//
|
|
7
|
+
// It seeds its controls from the running game's config (received over the bus), applies
|
|
8
|
+
// every edit LIVE (posts a patch → the game's reel bridge calls system.update), and
|
|
9
|
+
// offers a "Copy config" button that emits the paste-ready overrides snippet.
|
|
10
|
+
|
|
11
|
+
import type { HarnessPanelMount } from '@energy8platform/harness/panel';
|
|
12
|
+
import type { ReelSystemConfig } from '../config/ReelSystemConfig';
|
|
13
|
+
import { buildControlPanel } from './controlPanel';
|
|
14
|
+
import { REEL_FIELD_SCHEMA, type SectionKey } from './fieldSchema';
|
|
15
|
+
import { emitReelConfigTs } from './configDiff';
|
|
16
|
+
import { REEL_APPLY, REEL_READY, REEL_REQUEST, type ReelDevMessage } from './protocol';
|
|
17
|
+
|
|
18
|
+
/** Config the harness plugin passes via ctx.config, e.g. { sections: { geometry: false } }. */
|
|
19
|
+
interface ReelPanelConfig {
|
|
20
|
+
/** Per-section visibility. Missing key = shown. */
|
|
21
|
+
sections?: Partial<Record<SectionKey, boolean>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CSS = `
|
|
25
|
+
.e8rp-wait { color: #6b7480; font-size: 12px; line-height: 1.6; padding: 8px 2px; }
|
|
26
|
+
.e8rp-bar { display: flex; gap: 8px; margin-bottom: 12px; }
|
|
27
|
+
.e8rp-bar button {
|
|
28
|
+
flex: 1 1 0; background: #1c2128; color: #cfd5dd; border: 1px solid #3a414c; border-radius: 8px;
|
|
29
|
+
padding: 8px; font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
.e8rp-bar button:hover { border-color: #4b5563; color: #fff; }
|
|
32
|
+
.e8rp-status { font-size: 11px; color: #6b7480; min-height: 14px; margin-bottom: 8px; }
|
|
33
|
+
.e8rp-controls .panel-section { border-top: 1px solid #1d222a; }
|
|
34
|
+
.e8rp-controls .section-head {
|
|
35
|
+
width: 100%; text-align: left; background: transparent; border: 0; color: #aab2bd;
|
|
36
|
+
font: inherit; font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
|
|
37
|
+
padding: 10px 2px 6px; cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
.e8rp-controls .section-body { display: flex; flex-direction: column; gap: 2px; padding-bottom: 6px; }
|
|
40
|
+
.e8rp-controls .control {
|
|
41
|
+
display: flex; align-items: center; gap: 8px; padding: 4px 2px; font-size: 12px; color: #d3d8df;
|
|
42
|
+
}
|
|
43
|
+
.e8rp-controls .control-label { flex: 1 1 auto; }
|
|
44
|
+
.e8rp-controls .control-value { width: 34px; text-align: right; color: #828b98; font-variant-numeric: tabular-nums; }
|
|
45
|
+
.e8rp-controls input[type=range] { flex: 0 0 120px; }
|
|
46
|
+
.e8rp-controls select { background: #1c2128; color: #e9ecf1; border: 1px solid #3a414c; border-radius: 6px; padding: 3px 6px; font: inherit; font-size: 12px; }
|
|
47
|
+
.e8rp-controls .control-toggle { justify-content: space-between; }
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
function injectCss(): void {
|
|
51
|
+
if (document.getElementById('e8-reel-panel-css')) return;
|
|
52
|
+
const style = document.createElement('style');
|
|
53
|
+
style.id = 'e8-reel-panel-css';
|
|
54
|
+
style.textContent = CSS;
|
|
55
|
+
document.head.appendChild(style);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const mount: HarnessPanelMount = (ctx) => {
|
|
59
|
+
injectCss();
|
|
60
|
+
const root = ctx.root;
|
|
61
|
+
|
|
62
|
+
// Section visibility from the plugin (reelDevtoolsPlugin({ sections: { geometry: false } })).
|
|
63
|
+
const panelCfg = (ctx.config ?? {}) as ReelPanelConfig;
|
|
64
|
+
const sectionOn = panelCfg.sections ?? {};
|
|
65
|
+
const schema = REEL_FIELD_SCHEMA.filter((s) => s.key == null || sectionOn[s.key] !== false);
|
|
66
|
+
root.innerHTML = '<p class="e8rp-wait">Waiting for the game’s reel bridge…<br/>(the game must call <code>mountReelDevBridge</code>.)</p>';
|
|
67
|
+
|
|
68
|
+
let working: ReelSystemConfig | null = null;
|
|
69
|
+
let built = false;
|
|
70
|
+
|
|
71
|
+
const status = (msg: string): void => {
|
|
72
|
+
const el = root.querySelector<HTMLElement>('.e8rp-status');
|
|
73
|
+
if (el) el.textContent = msg;
|
|
74
|
+
};
|
|
75
|
+
const copy = (text: string, label: string): void => {
|
|
76
|
+
navigator.clipboard
|
|
77
|
+
?.writeText(text)
|
|
78
|
+
.then(() => status(`${label} copied`))
|
|
79
|
+
.catch(() => status('Copy failed'));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildUi = (config: ReelSystemConfig): void => {
|
|
83
|
+
working = structuredClone(config);
|
|
84
|
+
root.innerHTML = '';
|
|
85
|
+
|
|
86
|
+
const bar = document.createElement('div');
|
|
87
|
+
bar.className = 'e8rp-bar';
|
|
88
|
+
const copyTs = document.createElement('button');
|
|
89
|
+
copyTs.textContent = 'Copy config';
|
|
90
|
+
copyTs.addEventListener('click', () => working && copy(emitReelConfigTs(working), 'TS config'));
|
|
91
|
+
const copyJson = document.createElement('button');
|
|
92
|
+
copyJson.textContent = 'Copy JSON';
|
|
93
|
+
copyJson.addEventListener('click', () => working && copy(JSON.stringify(working, null, 2), 'JSON'));
|
|
94
|
+
bar.append(copyTs, copyJson);
|
|
95
|
+
|
|
96
|
+
const statusEl = document.createElement('div');
|
|
97
|
+
statusEl.className = 'e8rp-status';
|
|
98
|
+
|
|
99
|
+
const controls = document.createElement('div');
|
|
100
|
+
controls.className = 'e8rp-controls';
|
|
101
|
+
|
|
102
|
+
root.append(bar, statusEl, controls);
|
|
103
|
+
|
|
104
|
+
buildControlPanel(controls, {
|
|
105
|
+
config: working,
|
|
106
|
+
schema,
|
|
107
|
+
onChange: () => {
|
|
108
|
+
ctx.post({ type: REEL_APPLY, patch: working as ReelSystemConfig } satisfies ReelDevMessage);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
built = true;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
ctx.on((raw) => {
|
|
115
|
+
const msg = raw as ReelDevMessage | undefined;
|
|
116
|
+
if (msg && typeof msg === 'object' && msg.type === REEL_READY) buildUi(msg.config);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Ask the game for its config (covers the game-loaded-first case); retry a couple times.
|
|
120
|
+
const request = (): void => ctx.post({ type: REEL_REQUEST } satisfies ReelDevMessage);
|
|
121
|
+
request();
|
|
122
|
+
window.setTimeout(() => !built && request(), 400);
|
|
123
|
+
window.setTimeout(() => !built && request(), 1200);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default mount;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/devtools/protocol.ts
|
|
2
|
+
//
|
|
3
|
+
// The postMessage protocol between the harness reel panel (parent window) and the
|
|
4
|
+
// reel dev bridge (game iframe). Pure constants + message types — no deps.
|
|
5
|
+
|
|
6
|
+
import type { DeepPartial, ReelSystemConfig } from '../config/ReelSystemConfig';
|
|
7
|
+
|
|
8
|
+
/** Game → panel: announces the current resolved config (on mount + on request). */
|
|
9
|
+
export interface ReelReadyMessage {
|
|
10
|
+
type: 'e8:reel:ready';
|
|
11
|
+
config: ReelSystemConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Panel → game: apply a partial config live (system.update). */
|
|
15
|
+
export interface ReelApplyMessage {
|
|
16
|
+
type: 'e8:reel:apply';
|
|
17
|
+
patch: DeepPartial<ReelSystemConfig>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Panel → game: (re)request the current config, e.g. when the panel opens late. */
|
|
21
|
+
export interface ReelRequestMessage {
|
|
22
|
+
type: 'e8:reel:request';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ReelDevMessage = ReelReadyMessage | ReelApplyMessage | ReelRequestMessage;
|
|
26
|
+
|
|
27
|
+
export const REEL_READY = 'e8:reel:ready';
|
|
28
|
+
export const REEL_APPLY = 'e8:reel:apply';
|
|
29
|
+
export const REEL_REQUEST = 'e8:reel:request';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/devtools/reelDevBridge.ts
|
|
2
|
+
//
|
|
3
|
+
// Game-side opt-in bridge for the harness reel panel. Call once, after creating the
|
|
4
|
+
// ReelSystem, to let the harness sidebar tune the reels LIVE:
|
|
5
|
+
//
|
|
6
|
+
// import { mountReelDevBridge } from '@energy8platform/game-engine/devtools';
|
|
7
|
+
// const bridge = mountReelDevBridge({ system });
|
|
8
|
+
//
|
|
9
|
+
// It announces the system's current config to the parent (the harness wrapper) and
|
|
10
|
+
// applies incoming patches via `system.update()`. Changes are EPHEMERAL — they live
|
|
11
|
+
// only in this running iframe and are lost on reload. To persist, use the panel's
|
|
12
|
+
// "Copy config" button and paste the snippet into your game's reel config.
|
|
13
|
+
//
|
|
14
|
+
// No-op outside an iframe (window.parent === window), so it is safe to leave in.
|
|
15
|
+
|
|
16
|
+
import type { DeepPartial, ReelSystemConfig } from '../config/ReelSystemConfig';
|
|
17
|
+
import { REEL_APPLY, REEL_READY, REEL_REQUEST, type ReelDevMessage } from './protocol';
|
|
18
|
+
|
|
19
|
+
/** Minimal ReelSystem surface the bridge needs (keeps it decoupled/testable). */
|
|
20
|
+
export interface ReelDevBridgeTarget {
|
|
21
|
+
readonly config: ReelSystemConfig;
|
|
22
|
+
update(partial: DeepPartial<ReelSystemConfig>): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ReelDevBridgeOptions {
|
|
26
|
+
system: ReelDevBridgeTarget;
|
|
27
|
+
/** postMessage targetOrigin for the parent. Default '*' (dev harness, same-origin). */
|
|
28
|
+
targetOrigin?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Guard: only mount when true. Defaults to `import.meta.env?.DEV` when available,
|
|
31
|
+
* else true. Pass `false` to hard-disable.
|
|
32
|
+
*/
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReelDevBridge {
|
|
37
|
+
/** Re-announce the current config to the parent. */
|
|
38
|
+
announce(): void;
|
|
39
|
+
/** Detach the message listener. */
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultEnabled(): boolean {
|
|
44
|
+
try {
|
|
45
|
+
// vite injects import.meta.env.DEV; guard so non-vite builds don't throw.
|
|
46
|
+
const env = (import.meta as unknown as { env?: { DEV?: boolean } }).env;
|
|
47
|
+
return env?.DEV ?? true;
|
|
48
|
+
} catch {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function mountReelDevBridge(opts: ReelDevBridgeOptions): ReelDevBridge {
|
|
54
|
+
const enabled = opts.enabled ?? defaultEnabled();
|
|
55
|
+
const noop: ReelDevBridge = { announce: () => {}, dispose: () => {} };
|
|
56
|
+
if (!enabled || typeof window === 'undefined' || window.parent === window) return noop;
|
|
57
|
+
|
|
58
|
+
const origin = opts.targetOrigin ?? '*';
|
|
59
|
+
const { system } = opts;
|
|
60
|
+
|
|
61
|
+
const announce = (): void => {
|
|
62
|
+
window.parent.postMessage({ type: REEL_READY, config: system.config } satisfies ReelDevMessage, origin);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onMessage = (e: MessageEvent): void => {
|
|
66
|
+
if (e.source !== window.parent) return;
|
|
67
|
+
const msg = e.data as ReelDevMessage | undefined;
|
|
68
|
+
if (!msg || typeof msg !== 'object') return;
|
|
69
|
+
if (msg.type === REEL_APPLY) system.update(msg.patch);
|
|
70
|
+
else if (msg.type === REEL_REQUEST) announce();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
window.addEventListener('message', onMessage);
|
|
74
|
+
announce(); // cover the panel-already-open case
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
announce,
|
|
78
|
+
dispose: () => window.removeEventListener('message', onMessage),
|
|
79
|
+
};
|
|
80
|
+
}
|