@energy8platform/shell 0.6.6 → 0.7.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/html.cjs.js +669 -105
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +204 -27
- package/dist/html.esm.js +662 -106
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +370 -18
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +196 -26
- package/dist/index.esm.js +363 -19
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +944 -278
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +199 -28
- package/dist/pixi.esm.js +937 -279
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +68 -16
- package/src/core/icon-names.ts +30 -0
- package/src/core/index.ts +4 -0
- package/src/core/menu.ts +301 -0
- package/src/core/popover.ts +81 -0
- package/src/core/renderer.ts +13 -10
- package/src/core/state.ts +2 -1
- package/src/core/types.ts +12 -6
- package/src/core/version.ts +1 -1
- package/src/ui/html/HtmlRenderer.ts +31 -8
- package/src/ui/html/components/GameInfo.ts +1 -1
- package/src/ui/html/components/Menu.ts +153 -0
- package/src/ui/html/icons.ts +16 -15
- package/src/ui/html/primitives.ts +142 -0
- package/src/ui/html/shell.css.ts +21 -1
- package/src/ui/pixi/PixiRenderer.ts +32 -14
- package/src/ui/pixi/components/BottomBar.ts +80 -9
- package/src/ui/pixi/components/GameInfo.ts +1 -1
- package/src/ui/pixi/components/Menu.ts +167 -0
- package/src/ui/pixi/context.ts +3 -2
- package/src/ui/pixi/icons.ts +16 -15
- package/src/ui/pixi/pixi-icon.ts +15 -3
- package/src/ui/pixi/primitives/controls.ts +46 -0
- package/src/ui/pixi/primitives/popover.ts +163 -0
- package/src/ui/pixi/primitives/scroll.ts +9 -5
- package/src/ui/html/components/Settings.ts +0 -68
- package/src/ui/pixi/components/Settings.ts +0 -132
package/dist/index.cjs.js
CHANGED
|
@@ -52,6 +52,240 @@ class EventEmitter {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// AUTO-GENERATED from ../../icons.svg by scripts/gen-icons-from-svg.mjs — do not edit by hand.
|
|
56
|
+
// Sharp monochrome icon set: each glyph is a 24×24 viewBox fragment using currentColor
|
|
57
|
+
// (hollow shapes use fill-rule="evenodd"). Coordinates are baked into the 24×24 space (no
|
|
58
|
+
// <g transform>), so the same fragment renders identically in the DOM <svg> and in Pixi's
|
|
59
|
+
// GraphicsContext.svg. To change an icon: edit icons.svg, then run
|
|
60
|
+
// `node scripts/gen-icons-from-svg.mjs`.
|
|
61
|
+
// The single glyph-name union, shared by core (menu items) and both renderers.
|
|
62
|
+
const ICON_NAMES = [
|
|
63
|
+
'spin',
|
|
64
|
+
'turbo1',
|
|
65
|
+
'autoplay',
|
|
66
|
+
'stop',
|
|
67
|
+
'menu',
|
|
68
|
+
'minus',
|
|
69
|
+
'plus',
|
|
70
|
+
'gift',
|
|
71
|
+
'info',
|
|
72
|
+
'soundOn',
|
|
73
|
+
'soundOff',
|
|
74
|
+
'close',
|
|
75
|
+
'back',
|
|
76
|
+
'chevronRight',
|
|
77
|
+
'ticket',
|
|
78
|
+
'turbo2',
|
|
79
|
+
'turboOff',
|
|
80
|
+
'chevronUp',
|
|
81
|
+
'chevronDown',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const PRESET_IDS = ['sound', 'music', 'sfx', 'gameInfo'];
|
|
85
|
+
function isPresetId(id) {
|
|
86
|
+
return PRESET_IDS.includes(id);
|
|
87
|
+
}
|
|
88
|
+
/** The rows shown when `ShellConfig.menu` is omitted — today's Settings content, minus master. */
|
|
89
|
+
const DEFAULT_MENU = [
|
|
90
|
+
{ id: 'sound' },
|
|
91
|
+
{ id: 'music' },
|
|
92
|
+
{ id: 'sfx' },
|
|
93
|
+
{ type: 'separator' },
|
|
94
|
+
{ id: 'gameInfo' },
|
|
95
|
+
];
|
|
96
|
+
const isSeparator = (i) => i.type === 'separator';
|
|
97
|
+
/** Range bounds with defaults: 0..1 like a volume slider, step = a twentieth of the span. */
|
|
98
|
+
function rangeBounds(item) {
|
|
99
|
+
const min = item.min ?? 0;
|
|
100
|
+
const max = item.max ?? 1;
|
|
101
|
+
const derivedStep = (max - min) / 20;
|
|
102
|
+
// A declared `step` must be a genuinely positive number. `??` alone doesn't catch this: an
|
|
103
|
+
// explicit 0 (or a negative value) is not null/undefined, so it would sail through unchanged —
|
|
104
|
+
// and a renderer's position math divides by it (Pixi's fromUnit: `(raw-min)/step`), turning the
|
|
105
|
+
// slider's value into NaN, which then reaches `state.menu` and the `settingChange` payload.
|
|
106
|
+
const step = item.step != null && item.step > 0 ? item.step : derivedStep;
|
|
107
|
+
return { min, max, step };
|
|
108
|
+
}
|
|
109
|
+
/** Initial values for CUSTOM items (presets keep their own homes). Values already in `prev` win, so
|
|
110
|
+
* a later `setMenu()` with the same ids does not reset what the player has changed. */
|
|
111
|
+
function seedMenuValues(items, prev = {}) {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const item of items) {
|
|
114
|
+
if (isSeparator(item))
|
|
115
|
+
continue;
|
|
116
|
+
const type = item.type;
|
|
117
|
+
if (!type || isPresetId(item.id))
|
|
118
|
+
continue;
|
|
119
|
+
// A previous value only carries over when its runtime type still matches this item's kind.
|
|
120
|
+
// Without this guard, reconfiguring the same id from a toggle to a range (or back) would seed
|
|
121
|
+
// a boolean into a slider's position math (or a stale number into a checkbox) — a legitimate
|
|
122
|
+
// reconfiguration, not a config error, so it falls through to the item's own default instead
|
|
123
|
+
// of warning.
|
|
124
|
+
const prevValue = prev[item.id];
|
|
125
|
+
if (type === 'toggle') {
|
|
126
|
+
out[item.id] = typeof prevValue === 'boolean' ? prevValue : (item.value ?? false);
|
|
127
|
+
}
|
|
128
|
+
else if (type === 'range') {
|
|
129
|
+
const r = item;
|
|
130
|
+
out[item.id] = typeof prevValue === 'number' ? prevValue : (r.value ?? rangeBounds(r).min);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
const percent = (v) => `${Math.round(v * 100)}%`;
|
|
136
|
+
function safeIcon(name) {
|
|
137
|
+
return name && ICON_NAMES.includes(name) ? name : undefined;
|
|
138
|
+
}
|
|
139
|
+
/** Every drop/collision warning goes through here so the message style stays uniform. */
|
|
140
|
+
function warn(message) {
|
|
141
|
+
console.warn(`[shell] ${message}`);
|
|
142
|
+
}
|
|
143
|
+
/** Expand the configured list into render-ready rows. A config mistake — an unknown preset id, an
|
|
144
|
+
* unrecognized custom `type`, or an invalid range span — is dropped with one warning rather than
|
|
145
|
+
* silently misbehaving: a typo must be visible, not silently invisible. A custom id that collides
|
|
146
|
+
* with a reserved preset id also warns, but keeps its row (see `custom()`). */
|
|
147
|
+
function resolveMenu(host) {
|
|
148
|
+
const rows = [];
|
|
149
|
+
for (const item of host.menu) {
|
|
150
|
+
if (isSeparator(item)) {
|
|
151
|
+
rows.push({ kind: 'separator' });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const type = item.type;
|
|
155
|
+
if (!type) {
|
|
156
|
+
const row = preset(host, item);
|
|
157
|
+
if (row)
|
|
158
|
+
rows.push(row);
|
|
159
|
+
else
|
|
160
|
+
warn(`unknown menu preset id "${item.id}" — item skipped`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const row = custom(host, item, type);
|
|
164
|
+
if (row)
|
|
165
|
+
rows.push(row);
|
|
166
|
+
}
|
|
167
|
+
return rows;
|
|
168
|
+
}
|
|
169
|
+
function preset(host, item) {
|
|
170
|
+
const disabled = item.disabled ?? false;
|
|
171
|
+
const label = host.t(item.label ?? DEFAULT_LABELS[item.id] ?? item.id);
|
|
172
|
+
switch (item.id) {
|
|
173
|
+
case 'sound':
|
|
174
|
+
return {
|
|
175
|
+
kind: 'toggle',
|
|
176
|
+
id: 'sound',
|
|
177
|
+
label,
|
|
178
|
+
disabled,
|
|
179
|
+
icon: (v) => safeIcon(item.icon) ?? (v ? 'soundOn' : 'soundOff'),
|
|
180
|
+
get: () => host.getMenuValue('sound') !== false,
|
|
181
|
+
set: (v) => host.setMenuValue('sound', v),
|
|
182
|
+
};
|
|
183
|
+
case 'music':
|
|
184
|
+
case 'sfx': {
|
|
185
|
+
const id = item.id;
|
|
186
|
+
return {
|
|
187
|
+
kind: 'range',
|
|
188
|
+
id,
|
|
189
|
+
label,
|
|
190
|
+
icon: safeIcon(item.icon),
|
|
191
|
+
disabled,
|
|
192
|
+
min: 0,
|
|
193
|
+
max: 1,
|
|
194
|
+
step: 0.05,
|
|
195
|
+
get: () => Number(host.getMenuValue(id) ?? 1),
|
|
196
|
+
set: (v) => host.setMenuValue(id, v),
|
|
197
|
+
format: percent,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case 'gameInfo':
|
|
201
|
+
return {
|
|
202
|
+
kind: 'button',
|
|
203
|
+
id: 'gameInfo',
|
|
204
|
+
label,
|
|
205
|
+
icon: safeIcon(item.icon) ?? 'info',
|
|
206
|
+
disabled,
|
|
207
|
+
chevron: true,
|
|
208
|
+
select: () => host.actions.openInfo(),
|
|
209
|
+
};
|
|
210
|
+
default:
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const DEFAULT_LABELS = {
|
|
215
|
+
sound: 'Sound',
|
|
216
|
+
music: 'Music',
|
|
217
|
+
sfx: 'SFX',
|
|
218
|
+
gameInfo: 'Game info',
|
|
219
|
+
};
|
|
220
|
+
function custom(host, item, type) {
|
|
221
|
+
// Same store, different semantics: the real preset's get() and a custom row's get() disagree
|
|
222
|
+
// (see the preset cases below vs. the toggle case here). Routing does not change — Task 4's
|
|
223
|
+
// ShellHost is what actually owns the value — this warning just makes the clash visible.
|
|
224
|
+
if (isPresetId(item.id)) {
|
|
225
|
+
warn(`menu item id "${item.id}" collides with a built-in preset id — preset ids are reserved`);
|
|
226
|
+
}
|
|
227
|
+
const disabled = item.disabled ?? false;
|
|
228
|
+
const label = host.t(item.label ?? item.id);
|
|
229
|
+
const icon = safeIcon(item.icon);
|
|
230
|
+
if (type === 'toggle') {
|
|
231
|
+
const it = item;
|
|
232
|
+
return {
|
|
233
|
+
kind: 'toggle',
|
|
234
|
+
id: it.id,
|
|
235
|
+
label,
|
|
236
|
+
disabled,
|
|
237
|
+
icon: () => icon,
|
|
238
|
+
get: () => host.getMenuValue(it.id) === true,
|
|
239
|
+
set: (v) => {
|
|
240
|
+
host.setMenuValue(it.id, v);
|
|
241
|
+
it.onChange?.(v);
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (type === 'range') {
|
|
246
|
+
const it = item;
|
|
247
|
+
const { min, max, step } = rangeBounds(it);
|
|
248
|
+
if (max <= min) {
|
|
249
|
+
// A renderer's position math ((value - min) / (max - min)) turns this into Infinity/NaN —
|
|
250
|
+
// drop the row instead, exactly like an unknown preset id.
|
|
251
|
+
warn(`menu item "${it.id}" has invalid range bounds (min ${min}, max ${max}) — item skipped`);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
kind: 'range',
|
|
256
|
+
id: it.id,
|
|
257
|
+
label,
|
|
258
|
+
icon,
|
|
259
|
+
disabled,
|
|
260
|
+
min,
|
|
261
|
+
max,
|
|
262
|
+
step,
|
|
263
|
+
get: () => Number(host.getMenuValue(it.id) ?? min),
|
|
264
|
+
set: (v) => {
|
|
265
|
+
host.setMenuValue(it.id, v);
|
|
266
|
+
it.onChange?.(v);
|
|
267
|
+
},
|
|
268
|
+
format: it.format ?? (min === 0 && max === 1 ? percent : (v) => String(v)),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (type === 'button') {
|
|
272
|
+
const it = item;
|
|
273
|
+
return {
|
|
274
|
+
kind: 'button',
|
|
275
|
+
id: it.id,
|
|
276
|
+
label,
|
|
277
|
+
icon,
|
|
278
|
+
disabled,
|
|
279
|
+
chevron: it.chevron ?? false,
|
|
280
|
+
select: () => it.onSelect?.(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// Neither toggle, range, nor button — a typo'd `type` used to fall through to an unconditional
|
|
284
|
+
// button row with no onSelect. Drop it visibly instead.
|
|
285
|
+
warn(`menu item "${item.id}" has unknown type "${type}" — item skipped`);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
55
289
|
function createInitialState(config) {
|
|
56
290
|
return {
|
|
57
291
|
mode: config.mode,
|
|
@@ -68,10 +302,10 @@ function createInitialState(config) {
|
|
|
68
302
|
bonus: null,
|
|
69
303
|
activeFeature: null,
|
|
70
304
|
volumes: {
|
|
71
|
-
master: clampVolume(config.volumes?.master),
|
|
72
305
|
music: clampVolume(config.volumes?.music),
|
|
73
306
|
sfx: clampVolume(config.volumes?.sfx),
|
|
74
307
|
},
|
|
308
|
+
menu: seedMenuValues(config.menu ?? DEFAULT_MENU),
|
|
75
309
|
};
|
|
76
310
|
}
|
|
77
311
|
/** Clamp a configured volume to 0..1, defaulting to full (1) when unset/invalid. */
|
|
@@ -1429,7 +1663,7 @@ class KeyboardController {
|
|
|
1429
1663
|
|
|
1430
1664
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1431
1665
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1432
|
-
const PACKAGE_VERSION = '0.
|
|
1666
|
+
const PACKAGE_VERSION = '0.7.0';
|
|
1433
1667
|
|
|
1434
1668
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1435
1669
|
function resolveConfig(config) {
|
|
@@ -1447,6 +1681,7 @@ function resolveConfig(config) {
|
|
|
1447
1681
|
theme: config.theme,
|
|
1448
1682
|
onBonusBuy: config.onBonusBuy,
|
|
1449
1683
|
volumes: config.volumes,
|
|
1684
|
+
menu: config.menu,
|
|
1450
1685
|
version: config.version ?? '1.0.0',
|
|
1451
1686
|
isSocial: config.isSocial ?? false,
|
|
1452
1687
|
replay: config.replay ?? config.mode === 'replay',
|
|
@@ -1467,8 +1702,9 @@ class ShellController extends EventEmitter {
|
|
|
1467
1702
|
i18n;
|
|
1468
1703
|
kbd;
|
|
1469
1704
|
overlay = null;
|
|
1470
|
-
|
|
1471
|
-
|
|
1705
|
+
menuItems;
|
|
1706
|
+
menuRefresh = null;
|
|
1707
|
+
overlayKind = null;
|
|
1472
1708
|
prevBalance;
|
|
1473
1709
|
prevWin;
|
|
1474
1710
|
destroyed = false;
|
|
@@ -1479,6 +1715,7 @@ class ShellController extends EventEmitter {
|
|
|
1479
1715
|
this.config = resolveConfig(config);
|
|
1480
1716
|
this.i18n = createI18n({ language: this.config.language, isSocial: this.config.isSocial });
|
|
1481
1717
|
this.state = createInitialState(this.config);
|
|
1718
|
+
this.menuItems = this.config.menu ?? DEFAULT_MENU;
|
|
1482
1719
|
this.tokens = resolveTheme(this.config.theme);
|
|
1483
1720
|
this.prevBalance = this.state.balance;
|
|
1484
1721
|
this.prevWin = this.state.win;
|
|
@@ -1612,14 +1849,21 @@ class ShellController extends EventEmitter {
|
|
|
1612
1849
|
show(req) {
|
|
1613
1850
|
this.closeModal();
|
|
1614
1851
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1852
|
+
this.overlayKind = this.overlay ? req.kind : null;
|
|
1615
1853
|
}
|
|
1854
|
+
/** Open the bar menu. Called again while it is open, it closes it — the burger toggles. */
|
|
1616
1855
|
openMenu() {
|
|
1856
|
+
if (this.overlayKind === 'menu') {
|
|
1857
|
+
this.closeModal();
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1617
1860
|
this.emit('menuOpen');
|
|
1618
|
-
this.
|
|
1861
|
+
this.show({ kind: 'menu' });
|
|
1619
1862
|
}
|
|
1863
|
+
/** @deprecated The Settings overlay is gone — this opens the bar menu. */
|
|
1620
1864
|
openSettings() {
|
|
1621
1865
|
this.emit('settingsOpen');
|
|
1622
|
-
this.
|
|
1866
|
+
this.openMenu();
|
|
1623
1867
|
}
|
|
1624
1868
|
openInfo() {
|
|
1625
1869
|
this.emit('infoOpen');
|
|
@@ -1651,35 +1895,72 @@ class ShellController extends EventEmitter {
|
|
|
1651
1895
|
if (!this.overlay)
|
|
1652
1896
|
return;
|
|
1653
1897
|
this.overlay = null;
|
|
1654
|
-
this.
|
|
1655
|
-
this.
|
|
1898
|
+
this.overlayKind = null;
|
|
1899
|
+
this.menuRefresh = null;
|
|
1656
1900
|
this.renderer.closeOverlay();
|
|
1657
1901
|
}
|
|
1658
1902
|
// ── sound ──────────────────────────────────────────────────────────────────
|
|
1659
1903
|
setSound(on) {
|
|
1660
1904
|
this.soundOn = on;
|
|
1661
1905
|
this.emit('settingChange', { key: 'sound', value: on });
|
|
1662
|
-
this.
|
|
1663
|
-
this.renderer.refreshSoundIcon?.(on);
|
|
1664
|
-
}
|
|
1665
|
-
setSoundRefresh(fn) {
|
|
1666
|
-
this.soundRefresh = fn;
|
|
1906
|
+
this.menuRefresh?.('sound', on);
|
|
1667
1907
|
}
|
|
1668
1908
|
// ── volume ─────────────────────────────────────────────────────────────────
|
|
1669
1909
|
getVolume(key) {
|
|
1670
1910
|
return this.state.volumes[key];
|
|
1671
1911
|
}
|
|
1672
1912
|
/** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
|
|
1673
|
-
* clamps, stores so a reopened
|
|
1674
|
-
* live-updates the slider if the
|
|
1913
|
+
* clamps, stores so a reopened menu popover reflects it, emits `settingChange`, and
|
|
1914
|
+
* live-updates the slider if the menu is currently open. */
|
|
1675
1915
|
setVolume(key, value) {
|
|
1676
1916
|
const v = Math.max(0, Math.min(1, value));
|
|
1677
1917
|
this.state.volumes[key] = v;
|
|
1678
1918
|
this.emit('settingChange', { key, value: v });
|
|
1679
|
-
this.
|
|
1919
|
+
this.menuRefresh?.(key, v);
|
|
1920
|
+
}
|
|
1921
|
+
// ── menu ───────────────────────────────────────────────────────────────────
|
|
1922
|
+
get menu() {
|
|
1923
|
+
return this.menuItems;
|
|
1924
|
+
}
|
|
1925
|
+
/** Replace the item list. Values of ids already in state are kept; new ids are seeded. */
|
|
1926
|
+
setMenu(items) {
|
|
1927
|
+
this.menuItems = items;
|
|
1928
|
+
this.state.menu = seedMenuValues(items, this.state.menu);
|
|
1929
|
+
if (this.overlayKind === 'menu')
|
|
1930
|
+
this.show({ kind: 'menu' });
|
|
1680
1931
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1932
|
+
getMenuValue(id) {
|
|
1933
|
+
if (id === 'sound')
|
|
1934
|
+
return this.soundOn;
|
|
1935
|
+
if (id === 'music' || id === 'sfx')
|
|
1936
|
+
return this.state.volumes[id];
|
|
1937
|
+
return this.state.menu[id];
|
|
1938
|
+
}
|
|
1939
|
+
/** Set a menu value. Presets route to their own homes so there is never a second copy. */
|
|
1940
|
+
setMenuValue(id, value) {
|
|
1941
|
+
if (id === 'sound') {
|
|
1942
|
+
this.setSound(value !== false);
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (id === 'music' || id === 'sfx') {
|
|
1946
|
+
this.setVolume(id, Number(value));
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const next = typeof value === 'number' ? this.clampRange(id, value) : value;
|
|
1950
|
+
this.state.menu[id] = next;
|
|
1951
|
+
this.emit('settingChange', { key: id, value: next });
|
|
1952
|
+
this.menuRefresh?.(id, next);
|
|
1953
|
+
}
|
|
1954
|
+
setMenuRefresh(fn) {
|
|
1955
|
+
this.menuRefresh = fn;
|
|
1956
|
+
}
|
|
1957
|
+
/** Clamp to the declared bounds of a custom `range` item (a non-range id passes through). */
|
|
1958
|
+
clampRange(id, value) {
|
|
1959
|
+
const item = this.menuItems.find((i) => i.id === id);
|
|
1960
|
+
if (!item || item.type !== 'range')
|
|
1961
|
+
return value;
|
|
1962
|
+
const { min, max } = rangeBounds(item);
|
|
1963
|
+
return Math.max(min, Math.min(max, value));
|
|
1683
1964
|
}
|
|
1684
1965
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1685
1966
|
activateFeature(bonus) {
|
|
@@ -1789,6 +2070,11 @@ class ShellController extends EventEmitter {
|
|
|
1789
2070
|
if (this.destroyed)
|
|
1790
2071
|
return Promise.resolve();
|
|
1791
2072
|
this.destroyed = true;
|
|
2073
|
+
// With the menu (or any overlay) open at teardown, `menuRefresh` / `overlay` / `overlayKind`
|
|
2074
|
+
// would otherwise survive the renderer's destroy — so a later setVolume()/setSound() call
|
|
2075
|
+
// invokes a stale row updater against already-destroyed Pixi Graphics (or a detached DOM node)
|
|
2076
|
+
// and throws. Run BEFORE the renderer teardown below, while it can still close cleanly.
|
|
2077
|
+
this.closeModal();
|
|
1792
2078
|
if (typeof document !== 'undefined') {
|
|
1793
2079
|
this.kbd?.detach();
|
|
1794
2080
|
document.removeEventListener('pointerdown', this.pullFocus, true);
|
|
@@ -1798,6 +2084,64 @@ class ShellController extends EventEmitter {
|
|
|
1798
2084
|
}
|
|
1799
2085
|
}
|
|
1800
2086
|
|
|
2087
|
+
/** Geometry for the bar-menu popover. Pure math over rectangles so the DOM and Pixi renderers
|
|
2088
|
+
* place it identically — the renderers only supply measured sizes and apply the result. */
|
|
2089
|
+
const POPOVER = {
|
|
2090
|
+
/** Keep-out from the surface edges. */
|
|
2091
|
+
margin: 8,
|
|
2092
|
+
/** Space between the anchor and the card. */
|
|
2093
|
+
gap: 8,
|
|
2094
|
+
/** Minimum distance from the arrow tip to either rounded corner. */
|
|
2095
|
+
arrowInset: 14,
|
|
2096
|
+
/** A card shorter than this does not fit — flip to the other side instead. */
|
|
2097
|
+
minH: 120,
|
|
2098
|
+
minW: 220,
|
|
2099
|
+
maxW: 320,
|
|
2100
|
+
};
|
|
2101
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
2102
|
+
/** Card width: content width clamped to [minW, maxW] and never wider than the surface. */
|
|
2103
|
+
function popoverWidth(surfaceW, contentW) {
|
|
2104
|
+
const hi = Math.min(POPOVER.maxW, surfaceW - POPOVER.margin * 2);
|
|
2105
|
+
return Math.max(0, clamp(contentW, Math.min(POPOVER.minW, hi), hi));
|
|
2106
|
+
}
|
|
2107
|
+
/** Place the card above the `anchor` (below if it does not fit), left-aligned to it and clamped
|
|
2108
|
+
* inside the surface. `anchor === null` (no bar / hidden shell) centres it, arrow off.
|
|
2109
|
+
*
|
|
2110
|
+
* `anchor` drives PLACEMENT (x, y, maxH, below) — normally the bar's whole plaque ("plate"), so the
|
|
2111
|
+
* card sits flush with the bar as a whole rather than with whichever control opened it. `pointer` is
|
|
2112
|
+
* the (optional) rect the ARROW points at — normally the burger button, which can sit anywhere
|
|
2113
|
+
* inside the plate. Defaults to `anchor` when omitted, so every caller that only ever had one rect
|
|
2114
|
+
* (i.e. every caller before `pointer` existed) keeps behaving exactly as it did before. */
|
|
2115
|
+
function placePopover(anchor, surface, size, pointer = null) {
|
|
2116
|
+
const { margin, gap, arrowInset, minH } = POPOVER;
|
|
2117
|
+
if (!anchor) {
|
|
2118
|
+
const maxH = Math.max(0, surface.h - margin * 2);
|
|
2119
|
+
const h = Math.min(size.h, maxH);
|
|
2120
|
+
const rawY = (surface.h - h) / 2;
|
|
2121
|
+
const y = clamp(rawY, margin, Math.max(margin, surface.h - h - margin));
|
|
2122
|
+
return {
|
|
2123
|
+
x: Math.max(margin, (surface.w - size.w) / 2),
|
|
2124
|
+
y,
|
|
2125
|
+
maxH,
|
|
2126
|
+
arrowX: -1,
|
|
2127
|
+
below: false,
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
const spaceAbove = anchor.y - gap - margin;
|
|
2131
|
+
const spaceBelow = surface.h - (anchor.y + anchor.h) - gap - margin;
|
|
2132
|
+
// Prefer above; flip only when the card would be squeezed below its usable minimum AND there is
|
|
2133
|
+
// genuinely more room on the other side.
|
|
2134
|
+
const below = spaceAbove < Math.min(size.h, minH) && spaceBelow > spaceAbove;
|
|
2135
|
+
const maxH = Math.max(0, below ? spaceBelow : spaceAbove);
|
|
2136
|
+
const h = Math.min(size.h, maxH);
|
|
2137
|
+
const x = clamp(anchor.x, margin, Math.max(margin, surface.w - size.w - margin));
|
|
2138
|
+
const rawY = below ? anchor.y + anchor.h + gap : anchor.y - gap - h;
|
|
2139
|
+
const y = clamp(rawY, margin, Math.max(margin, surface.h - h - margin));
|
|
2140
|
+
const arrowAnchor = pointer ?? anchor;
|
|
2141
|
+
const arrowX = clamp(arrowAnchor.x + arrowAnchor.w / 2 - x, arrowInset, Math.max(arrowInset, size.w - arrowInset));
|
|
2142
|
+
return { x, y, maxH, arrowX, below };
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1801
2145
|
const NO_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
1802
2146
|
/** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
|
|
1803
2147
|
* Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
|
|
@@ -1817,13 +2161,21 @@ function createShell(opts) {
|
|
|
1817
2161
|
}
|
|
1818
2162
|
|
|
1819
2163
|
exports.DEFAULT_ACCENT = DEFAULT_ACCENT;
|
|
2164
|
+
exports.DEFAULT_MENU = DEFAULT_MENU;
|
|
1820
2165
|
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
|
2166
|
+
exports.POPOVER = POPOVER;
|
|
1821
2167
|
exports.SCHEMES = SCHEMES;
|
|
1822
2168
|
exports.ShellController = ShellController;
|
|
1823
2169
|
exports.createI18n = createI18n;
|
|
1824
2170
|
exports.createShell = createShell;
|
|
2171
|
+
exports.isPresetId = isPresetId;
|
|
1825
2172
|
exports.normalizeLang = normalizeLang;
|
|
2173
|
+
exports.placePopover = placePopover;
|
|
2174
|
+
exports.popoverWidth = popoverWidth;
|
|
2175
|
+
exports.rangeBounds = rangeBounds;
|
|
1826
2176
|
exports.resolveConfig = resolveConfig;
|
|
2177
|
+
exports.resolveMenu = resolveMenu;
|
|
1827
2178
|
exports.resolveTheme = resolveTheme;
|
|
2179
|
+
exports.seedMenuValues = seedMenuValues;
|
|
1828
2180
|
exports.socialize = socialize;
|
|
1829
2181
|
//# sourceMappingURL=index.cjs.js.map
|