@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/pixi.cjs.js
CHANGED
|
@@ -53,6 +53,240 @@ class EventEmitter {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// AUTO-GENERATED from ../../icons.svg by scripts/gen-icons-from-svg.mjs — do not edit by hand.
|
|
57
|
+
// Sharp monochrome icon set: each glyph is a 24×24 viewBox fragment using currentColor
|
|
58
|
+
// (hollow shapes use fill-rule="evenodd"). Coordinates are baked into the 24×24 space (no
|
|
59
|
+
// <g transform>), so the same fragment renders identically in the DOM <svg> and in Pixi's
|
|
60
|
+
// GraphicsContext.svg. To change an icon: edit icons.svg, then run
|
|
61
|
+
// `node scripts/gen-icons-from-svg.mjs`.
|
|
62
|
+
// The single glyph-name union, shared by core (menu items) and both renderers.
|
|
63
|
+
const ICON_NAMES = [
|
|
64
|
+
'spin',
|
|
65
|
+
'turbo1',
|
|
66
|
+
'autoplay',
|
|
67
|
+
'stop',
|
|
68
|
+
'menu',
|
|
69
|
+
'minus',
|
|
70
|
+
'plus',
|
|
71
|
+
'gift',
|
|
72
|
+
'info',
|
|
73
|
+
'soundOn',
|
|
74
|
+
'soundOff',
|
|
75
|
+
'close',
|
|
76
|
+
'back',
|
|
77
|
+
'chevronRight',
|
|
78
|
+
'ticket',
|
|
79
|
+
'turbo2',
|
|
80
|
+
'turboOff',
|
|
81
|
+
'chevronUp',
|
|
82
|
+
'chevronDown',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const PRESET_IDS = ['sound', 'music', 'sfx', 'gameInfo'];
|
|
86
|
+
function isPresetId(id) {
|
|
87
|
+
return PRESET_IDS.includes(id);
|
|
88
|
+
}
|
|
89
|
+
/** The rows shown when `ShellConfig.menu` is omitted — today's Settings content, minus master. */
|
|
90
|
+
const DEFAULT_MENU = [
|
|
91
|
+
{ id: 'sound' },
|
|
92
|
+
{ id: 'music' },
|
|
93
|
+
{ id: 'sfx' },
|
|
94
|
+
{ type: 'separator' },
|
|
95
|
+
{ id: 'gameInfo' },
|
|
96
|
+
];
|
|
97
|
+
const isSeparator = (i) => i.type === 'separator';
|
|
98
|
+
/** Range bounds with defaults: 0..1 like a volume slider, step = a twentieth of the span. */
|
|
99
|
+
function rangeBounds(item) {
|
|
100
|
+
const min = item.min ?? 0;
|
|
101
|
+
const max = item.max ?? 1;
|
|
102
|
+
const derivedStep = (max - min) / 20;
|
|
103
|
+
// A declared `step` must be a genuinely positive number. `??` alone doesn't catch this: an
|
|
104
|
+
// explicit 0 (or a negative value) is not null/undefined, so it would sail through unchanged —
|
|
105
|
+
// and a renderer's position math divides by it (Pixi's fromUnit: `(raw-min)/step`), turning the
|
|
106
|
+
// slider's value into NaN, which then reaches `state.menu` and the `settingChange` payload.
|
|
107
|
+
const step = item.step != null && item.step > 0 ? item.step : derivedStep;
|
|
108
|
+
return { min, max, step };
|
|
109
|
+
}
|
|
110
|
+
/** Initial values for CUSTOM items (presets keep their own homes). Values already in `prev` win, so
|
|
111
|
+
* a later `setMenu()` with the same ids does not reset what the player has changed. */
|
|
112
|
+
function seedMenuValues(items, prev = {}) {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
if (isSeparator(item))
|
|
116
|
+
continue;
|
|
117
|
+
const type = item.type;
|
|
118
|
+
if (!type || isPresetId(item.id))
|
|
119
|
+
continue;
|
|
120
|
+
// A previous value only carries over when its runtime type still matches this item's kind.
|
|
121
|
+
// Without this guard, reconfiguring the same id from a toggle to a range (or back) would seed
|
|
122
|
+
// a boolean into a slider's position math (or a stale number into a checkbox) — a legitimate
|
|
123
|
+
// reconfiguration, not a config error, so it falls through to the item's own default instead
|
|
124
|
+
// of warning.
|
|
125
|
+
const prevValue = prev[item.id];
|
|
126
|
+
if (type === 'toggle') {
|
|
127
|
+
out[item.id] = typeof prevValue === 'boolean' ? prevValue : (item.value ?? false);
|
|
128
|
+
}
|
|
129
|
+
else if (type === 'range') {
|
|
130
|
+
const r = item;
|
|
131
|
+
out[item.id] = typeof prevValue === 'number' ? prevValue : (r.value ?? rangeBounds(r).min);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
const percent = (v) => `${Math.round(v * 100)}%`;
|
|
137
|
+
function safeIcon(name) {
|
|
138
|
+
return name && ICON_NAMES.includes(name) ? name : undefined;
|
|
139
|
+
}
|
|
140
|
+
/** Every drop/collision warning goes through here so the message style stays uniform. */
|
|
141
|
+
function warn(message) {
|
|
142
|
+
console.warn(`[shell] ${message}`);
|
|
143
|
+
}
|
|
144
|
+
/** Expand the configured list into render-ready rows. A config mistake — an unknown preset id, an
|
|
145
|
+
* unrecognized custom `type`, or an invalid range span — is dropped with one warning rather than
|
|
146
|
+
* silently misbehaving: a typo must be visible, not silently invisible. A custom id that collides
|
|
147
|
+
* with a reserved preset id also warns, but keeps its row (see `custom()`). */
|
|
148
|
+
function resolveMenu(host) {
|
|
149
|
+
const rows = [];
|
|
150
|
+
for (const item of host.menu) {
|
|
151
|
+
if (isSeparator(item)) {
|
|
152
|
+
rows.push({ kind: 'separator' });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const type = item.type;
|
|
156
|
+
if (!type) {
|
|
157
|
+
const row = preset(host, item);
|
|
158
|
+
if (row)
|
|
159
|
+
rows.push(row);
|
|
160
|
+
else
|
|
161
|
+
warn(`unknown menu preset id "${item.id}" — item skipped`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const row = custom(host, item, type);
|
|
165
|
+
if (row)
|
|
166
|
+
rows.push(row);
|
|
167
|
+
}
|
|
168
|
+
return rows;
|
|
169
|
+
}
|
|
170
|
+
function preset(host, item) {
|
|
171
|
+
const disabled = item.disabled ?? false;
|
|
172
|
+
const label = host.t(item.label ?? DEFAULT_LABELS[item.id] ?? item.id);
|
|
173
|
+
switch (item.id) {
|
|
174
|
+
case 'sound':
|
|
175
|
+
return {
|
|
176
|
+
kind: 'toggle',
|
|
177
|
+
id: 'sound',
|
|
178
|
+
label,
|
|
179
|
+
disabled,
|
|
180
|
+
icon: (v) => safeIcon(item.icon) ?? (v ? 'soundOn' : 'soundOff'),
|
|
181
|
+
get: () => host.getMenuValue('sound') !== false,
|
|
182
|
+
set: (v) => host.setMenuValue('sound', v),
|
|
183
|
+
};
|
|
184
|
+
case 'music':
|
|
185
|
+
case 'sfx': {
|
|
186
|
+
const id = item.id;
|
|
187
|
+
return {
|
|
188
|
+
kind: 'range',
|
|
189
|
+
id,
|
|
190
|
+
label,
|
|
191
|
+
icon: safeIcon(item.icon),
|
|
192
|
+
disabled,
|
|
193
|
+
min: 0,
|
|
194
|
+
max: 1,
|
|
195
|
+
step: 0.05,
|
|
196
|
+
get: () => Number(host.getMenuValue(id) ?? 1),
|
|
197
|
+
set: (v) => host.setMenuValue(id, v),
|
|
198
|
+
format: percent,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case 'gameInfo':
|
|
202
|
+
return {
|
|
203
|
+
kind: 'button',
|
|
204
|
+
id: 'gameInfo',
|
|
205
|
+
label,
|
|
206
|
+
icon: safeIcon(item.icon) ?? 'info',
|
|
207
|
+
disabled,
|
|
208
|
+
chevron: true,
|
|
209
|
+
select: () => host.actions.openInfo(),
|
|
210
|
+
};
|
|
211
|
+
default:
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const DEFAULT_LABELS = {
|
|
216
|
+
sound: 'Sound',
|
|
217
|
+
music: 'Music',
|
|
218
|
+
sfx: 'SFX',
|
|
219
|
+
gameInfo: 'Game info',
|
|
220
|
+
};
|
|
221
|
+
function custom(host, item, type) {
|
|
222
|
+
// Same store, different semantics: the real preset's get() and a custom row's get() disagree
|
|
223
|
+
// (see the preset cases below vs. the toggle case here). Routing does not change — Task 4's
|
|
224
|
+
// ShellHost is what actually owns the value — this warning just makes the clash visible.
|
|
225
|
+
if (isPresetId(item.id)) {
|
|
226
|
+
warn(`menu item id "${item.id}" collides with a built-in preset id — preset ids are reserved`);
|
|
227
|
+
}
|
|
228
|
+
const disabled = item.disabled ?? false;
|
|
229
|
+
const label = host.t(item.label ?? item.id);
|
|
230
|
+
const icon = safeIcon(item.icon);
|
|
231
|
+
if (type === 'toggle') {
|
|
232
|
+
const it = item;
|
|
233
|
+
return {
|
|
234
|
+
kind: 'toggle',
|
|
235
|
+
id: it.id,
|
|
236
|
+
label,
|
|
237
|
+
disabled,
|
|
238
|
+
icon: () => icon,
|
|
239
|
+
get: () => host.getMenuValue(it.id) === true,
|
|
240
|
+
set: (v) => {
|
|
241
|
+
host.setMenuValue(it.id, v);
|
|
242
|
+
it.onChange?.(v);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (type === 'range') {
|
|
247
|
+
const it = item;
|
|
248
|
+
const { min, max, step } = rangeBounds(it);
|
|
249
|
+
if (max <= min) {
|
|
250
|
+
// A renderer's position math ((value - min) / (max - min)) turns this into Infinity/NaN —
|
|
251
|
+
// drop the row instead, exactly like an unknown preset id.
|
|
252
|
+
warn(`menu item "${it.id}" has invalid range bounds (min ${min}, max ${max}) — item skipped`);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
kind: 'range',
|
|
257
|
+
id: it.id,
|
|
258
|
+
label,
|
|
259
|
+
icon,
|
|
260
|
+
disabled,
|
|
261
|
+
min,
|
|
262
|
+
max,
|
|
263
|
+
step,
|
|
264
|
+
get: () => Number(host.getMenuValue(it.id) ?? min),
|
|
265
|
+
set: (v) => {
|
|
266
|
+
host.setMenuValue(it.id, v);
|
|
267
|
+
it.onChange?.(v);
|
|
268
|
+
},
|
|
269
|
+
format: it.format ?? (min === 0 && max === 1 ? percent : (v) => String(v)),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (type === 'button') {
|
|
273
|
+
const it = item;
|
|
274
|
+
return {
|
|
275
|
+
kind: 'button',
|
|
276
|
+
id: it.id,
|
|
277
|
+
label,
|
|
278
|
+
icon,
|
|
279
|
+
disabled,
|
|
280
|
+
chevron: it.chevron ?? false,
|
|
281
|
+
select: () => it.onSelect?.(),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// Neither toggle, range, nor button — a typo'd `type` used to fall through to an unconditional
|
|
285
|
+
// button row with no onSelect. Drop it visibly instead.
|
|
286
|
+
warn(`menu item "${item.id}" has unknown type "${type}" — item skipped`);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
56
290
|
function createInitialState(config) {
|
|
57
291
|
return {
|
|
58
292
|
mode: config.mode,
|
|
@@ -69,10 +303,10 @@ function createInitialState(config) {
|
|
|
69
303
|
bonus: null,
|
|
70
304
|
activeFeature: null,
|
|
71
305
|
volumes: {
|
|
72
|
-
master: clampVolume(config.volumes?.master),
|
|
73
306
|
music: clampVolume(config.volumes?.music),
|
|
74
307
|
sfx: clampVolume(config.volumes?.sfx),
|
|
75
308
|
},
|
|
309
|
+
menu: seedMenuValues(config.menu ?? DEFAULT_MENU),
|
|
76
310
|
};
|
|
77
311
|
}
|
|
78
312
|
/** Clamp a configured volume to 0..1, defaulting to full (1) when unset/invalid. */
|
|
@@ -1456,7 +1690,7 @@ class KeyboardController {
|
|
|
1456
1690
|
|
|
1457
1691
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1458
1692
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1459
|
-
const PACKAGE_VERSION = '0.
|
|
1693
|
+
const PACKAGE_VERSION = '0.7.0';
|
|
1460
1694
|
|
|
1461
1695
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1462
1696
|
function resolveConfig(config) {
|
|
@@ -1474,6 +1708,7 @@ function resolveConfig(config) {
|
|
|
1474
1708
|
theme: config.theme,
|
|
1475
1709
|
onBonusBuy: config.onBonusBuy,
|
|
1476
1710
|
volumes: config.volumes,
|
|
1711
|
+
menu: config.menu,
|
|
1477
1712
|
version: config.version ?? '1.0.0',
|
|
1478
1713
|
isSocial: config.isSocial ?? false,
|
|
1479
1714
|
replay: config.replay ?? config.mode === 'replay',
|
|
@@ -1494,8 +1729,9 @@ class ShellController extends EventEmitter {
|
|
|
1494
1729
|
i18n;
|
|
1495
1730
|
kbd;
|
|
1496
1731
|
overlay = null;
|
|
1497
|
-
|
|
1498
|
-
|
|
1732
|
+
menuItems;
|
|
1733
|
+
menuRefresh = null;
|
|
1734
|
+
overlayKind = null;
|
|
1499
1735
|
prevBalance;
|
|
1500
1736
|
prevWin;
|
|
1501
1737
|
destroyed = false;
|
|
@@ -1506,6 +1742,7 @@ class ShellController extends EventEmitter {
|
|
|
1506
1742
|
this.config = resolveConfig(config);
|
|
1507
1743
|
this.i18n = createI18n({ language: this.config.language, isSocial: this.config.isSocial });
|
|
1508
1744
|
this.state = createInitialState(this.config);
|
|
1745
|
+
this.menuItems = this.config.menu ?? DEFAULT_MENU;
|
|
1509
1746
|
this.tokens = resolveTheme(this.config.theme);
|
|
1510
1747
|
this.prevBalance = this.state.balance;
|
|
1511
1748
|
this.prevWin = this.state.win;
|
|
@@ -1639,14 +1876,21 @@ class ShellController extends EventEmitter {
|
|
|
1639
1876
|
show(req) {
|
|
1640
1877
|
this.closeModal();
|
|
1641
1878
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1879
|
+
this.overlayKind = this.overlay ? req.kind : null;
|
|
1642
1880
|
}
|
|
1881
|
+
/** Open the bar menu. Called again while it is open, it closes it — the burger toggles. */
|
|
1643
1882
|
openMenu() {
|
|
1883
|
+
if (this.overlayKind === 'menu') {
|
|
1884
|
+
this.closeModal();
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1644
1887
|
this.emit('menuOpen');
|
|
1645
|
-
this.
|
|
1888
|
+
this.show({ kind: 'menu' });
|
|
1646
1889
|
}
|
|
1890
|
+
/** @deprecated The Settings overlay is gone — this opens the bar menu. */
|
|
1647
1891
|
openSettings() {
|
|
1648
1892
|
this.emit('settingsOpen');
|
|
1649
|
-
this.
|
|
1893
|
+
this.openMenu();
|
|
1650
1894
|
}
|
|
1651
1895
|
openInfo() {
|
|
1652
1896
|
this.emit('infoOpen');
|
|
@@ -1678,35 +1922,72 @@ class ShellController extends EventEmitter {
|
|
|
1678
1922
|
if (!this.overlay)
|
|
1679
1923
|
return;
|
|
1680
1924
|
this.overlay = null;
|
|
1681
|
-
this.
|
|
1682
|
-
this.
|
|
1925
|
+
this.overlayKind = null;
|
|
1926
|
+
this.menuRefresh = null;
|
|
1683
1927
|
this.renderer.closeOverlay();
|
|
1684
1928
|
}
|
|
1685
1929
|
// ── sound ──────────────────────────────────────────────────────────────────
|
|
1686
1930
|
setSound(on) {
|
|
1687
1931
|
this.soundOn = on;
|
|
1688
1932
|
this.emit('settingChange', { key: 'sound', value: on });
|
|
1689
|
-
this.
|
|
1690
|
-
this.renderer.refreshSoundIcon?.(on);
|
|
1691
|
-
}
|
|
1692
|
-
setSoundRefresh(fn) {
|
|
1693
|
-
this.soundRefresh = fn;
|
|
1933
|
+
this.menuRefresh?.('sound', on);
|
|
1694
1934
|
}
|
|
1695
1935
|
// ── volume ─────────────────────────────────────────────────────────────────
|
|
1696
1936
|
getVolume(key) {
|
|
1697
1937
|
return this.state.volumes[key];
|
|
1698
1938
|
}
|
|
1699
1939
|
/** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
|
|
1700
|
-
* clamps, stores so a reopened
|
|
1701
|
-
* live-updates the slider if the
|
|
1940
|
+
* clamps, stores so a reopened menu popover reflects it, emits `settingChange`, and
|
|
1941
|
+
* live-updates the slider if the menu is currently open. */
|
|
1702
1942
|
setVolume(key, value) {
|
|
1703
1943
|
const v = Math.max(0, Math.min(1, value));
|
|
1704
1944
|
this.state.volumes[key] = v;
|
|
1705
1945
|
this.emit('settingChange', { key, value: v });
|
|
1706
|
-
this.
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1946
|
+
this.menuRefresh?.(key, v);
|
|
1947
|
+
}
|
|
1948
|
+
// ── menu ───────────────────────────────────────────────────────────────────
|
|
1949
|
+
get menu() {
|
|
1950
|
+
return this.menuItems;
|
|
1951
|
+
}
|
|
1952
|
+
/** Replace the item list. Values of ids already in state are kept; new ids are seeded. */
|
|
1953
|
+
setMenu(items) {
|
|
1954
|
+
this.menuItems = items;
|
|
1955
|
+
this.state.menu = seedMenuValues(items, this.state.menu);
|
|
1956
|
+
if (this.overlayKind === 'menu')
|
|
1957
|
+
this.show({ kind: 'menu' });
|
|
1958
|
+
}
|
|
1959
|
+
getMenuValue(id) {
|
|
1960
|
+
if (id === 'sound')
|
|
1961
|
+
return this.soundOn;
|
|
1962
|
+
if (id === 'music' || id === 'sfx')
|
|
1963
|
+
return this.state.volumes[id];
|
|
1964
|
+
return this.state.menu[id];
|
|
1965
|
+
}
|
|
1966
|
+
/** Set a menu value. Presets route to their own homes so there is never a second copy. */
|
|
1967
|
+
setMenuValue(id, value) {
|
|
1968
|
+
if (id === 'sound') {
|
|
1969
|
+
this.setSound(value !== false);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
if (id === 'music' || id === 'sfx') {
|
|
1973
|
+
this.setVolume(id, Number(value));
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const next = typeof value === 'number' ? this.clampRange(id, value) : value;
|
|
1977
|
+
this.state.menu[id] = next;
|
|
1978
|
+
this.emit('settingChange', { key: id, value: next });
|
|
1979
|
+
this.menuRefresh?.(id, next);
|
|
1980
|
+
}
|
|
1981
|
+
setMenuRefresh(fn) {
|
|
1982
|
+
this.menuRefresh = fn;
|
|
1983
|
+
}
|
|
1984
|
+
/** Clamp to the declared bounds of a custom `range` item (a non-range id passes through). */
|
|
1985
|
+
clampRange(id, value) {
|
|
1986
|
+
const item = this.menuItems.find((i) => i.id === id);
|
|
1987
|
+
if (!item || item.type !== 'range')
|
|
1988
|
+
return value;
|
|
1989
|
+
const { min, max } = rangeBounds(item);
|
|
1990
|
+
return Math.max(min, Math.min(max, value));
|
|
1710
1991
|
}
|
|
1711
1992
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1712
1993
|
activateFeature(bonus) {
|
|
@@ -1816,6 +2097,11 @@ class ShellController extends EventEmitter {
|
|
|
1816
2097
|
if (this.destroyed)
|
|
1817
2098
|
return Promise.resolve();
|
|
1818
2099
|
this.destroyed = true;
|
|
2100
|
+
// With the menu (or any overlay) open at teardown, `menuRefresh` / `overlay` / `overlayKind`
|
|
2101
|
+
// would otherwise survive the renderer's destroy — so a later setVolume()/setSound() call
|
|
2102
|
+
// invokes a stale row updater against already-destroyed Pixi Graphics (or a detached DOM node)
|
|
2103
|
+
// and throws. Run BEFORE the renderer teardown below, while it can still close cleanly.
|
|
2104
|
+
this.closeModal();
|
|
1819
2105
|
if (typeof document !== 'undefined') {
|
|
1820
2106
|
this.kbd?.detach();
|
|
1821
2107
|
document.removeEventListener('pointerdown', this.pullFocus, true);
|
|
@@ -1825,6 +2111,64 @@ class ShellController extends EventEmitter {
|
|
|
1825
2111
|
}
|
|
1826
2112
|
}
|
|
1827
2113
|
|
|
2114
|
+
/** Geometry for the bar-menu popover. Pure math over rectangles so the DOM and Pixi renderers
|
|
2115
|
+
* place it identically — the renderers only supply measured sizes and apply the result. */
|
|
2116
|
+
const POPOVER = {
|
|
2117
|
+
/** Keep-out from the surface edges. */
|
|
2118
|
+
margin: 8,
|
|
2119
|
+
/** Space between the anchor and the card. */
|
|
2120
|
+
gap: 8,
|
|
2121
|
+
/** Minimum distance from the arrow tip to either rounded corner. */
|
|
2122
|
+
arrowInset: 14,
|
|
2123
|
+
/** A card shorter than this does not fit — flip to the other side instead. */
|
|
2124
|
+
minH: 120,
|
|
2125
|
+
minW: 220,
|
|
2126
|
+
maxW: 320,
|
|
2127
|
+
};
|
|
2128
|
+
const clamp$1 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
2129
|
+
/** Card width: content width clamped to [minW, maxW] and never wider than the surface. */
|
|
2130
|
+
function popoverWidth(surfaceW, contentW) {
|
|
2131
|
+
const hi = Math.min(POPOVER.maxW, surfaceW - POPOVER.margin * 2);
|
|
2132
|
+
return Math.max(0, clamp$1(contentW, Math.min(POPOVER.minW, hi), hi));
|
|
2133
|
+
}
|
|
2134
|
+
/** Place the card above the `anchor` (below if it does not fit), left-aligned to it and clamped
|
|
2135
|
+
* inside the surface. `anchor === null` (no bar / hidden shell) centres it, arrow off.
|
|
2136
|
+
*
|
|
2137
|
+
* `anchor` drives PLACEMENT (x, y, maxH, below) — normally the bar's whole plaque ("plate"), so the
|
|
2138
|
+
* card sits flush with the bar as a whole rather than with whichever control opened it. `pointer` is
|
|
2139
|
+
* the (optional) rect the ARROW points at — normally the burger button, which can sit anywhere
|
|
2140
|
+
* inside the plate. Defaults to `anchor` when omitted, so every caller that only ever had one rect
|
|
2141
|
+
* (i.e. every caller before `pointer` existed) keeps behaving exactly as it did before. */
|
|
2142
|
+
function placePopover(anchor, surface, size, pointer = null) {
|
|
2143
|
+
const { margin, gap, arrowInset, minH } = POPOVER;
|
|
2144
|
+
if (!anchor) {
|
|
2145
|
+
const maxH = Math.max(0, surface.h - margin * 2);
|
|
2146
|
+
const h = Math.min(size.h, maxH);
|
|
2147
|
+
const rawY = (surface.h - h) / 2;
|
|
2148
|
+
const y = clamp$1(rawY, margin, Math.max(margin, surface.h - h - margin));
|
|
2149
|
+
return {
|
|
2150
|
+
x: Math.max(margin, (surface.w - size.w) / 2),
|
|
2151
|
+
y,
|
|
2152
|
+
maxH,
|
|
2153
|
+
arrowX: -1,
|
|
2154
|
+
below: false,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
const spaceAbove = anchor.y - gap - margin;
|
|
2158
|
+
const spaceBelow = surface.h - (anchor.y + anchor.h) - gap - margin;
|
|
2159
|
+
// Prefer above; flip only when the card would be squeezed below its usable minimum AND there is
|
|
2160
|
+
// genuinely more room on the other side.
|
|
2161
|
+
const below = spaceAbove < Math.min(size.h, minH) && spaceBelow > spaceAbove;
|
|
2162
|
+
const maxH = Math.max(0, below ? spaceBelow : spaceAbove);
|
|
2163
|
+
const h = Math.min(size.h, maxH);
|
|
2164
|
+
const x = clamp$1(anchor.x, margin, Math.max(margin, surface.w - size.w - margin));
|
|
2165
|
+
const rawY = below ? anchor.y + anchor.h + gap : anchor.y - gap - h;
|
|
2166
|
+
const y = clamp$1(rawY, margin, Math.max(margin, surface.h - h - margin));
|
|
2167
|
+
const arrowAnchor = pointer ?? anchor;
|
|
2168
|
+
const arrowX = clamp$1(arrowAnchor.x + arrowAnchor.w / 2 - x, arrowInset, Math.max(arrowInset, size.w - arrowInset));
|
|
2169
|
+
return { x, y, maxH, arrowX, below };
|
|
2170
|
+
}
|
|
2171
|
+
|
|
1828
2172
|
const NO_INSET = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
1829
2173
|
/** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
|
|
1830
2174
|
* Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
|
|
@@ -2294,25 +2638,25 @@ function roundedPath(g, x, y, w, h, [tl, tr, br, bl]) {
|
|
|
2294
2638
|
// GraphicsContext.svg. To change an icon: edit icons.svg, then run
|
|
2295
2639
|
// `node scripts/gen-icons-from-svg.mjs`.
|
|
2296
2640
|
const SVGS = {
|
|
2297
|
-
spin: `<g fill="currentColor" fill-rule="evenodd"><path d="M13.57 8.41C13.3 8.31 13.51 7.09 13.59 6.65C13.61 6.56 13.55 6.48 13.46 6.48C12.32 6.45 5.74 6.61 4.16 14.85C4.15 14.9 4 15.16 3.8 14.91C3.27 14.23 2.43 12.08 3.46 9.16C6.01 1.92 13 3.07 13.79 3.23C13.83 3.23 13.86 3.2 13.86 3.17C13.85 2.86 13.82 1.68 14.17 1.64C14.51 1.6 19.04 5.61 18.96 5.95C18.86 6.36 13.83 8.51 13.57 8.41Z"/><path d="M10.43 15.59C10.7 15.69 10.49 16.91 10.41 17.35C10.39 17.44 10.45 17.52 10.54 17.52C11.68 17.55 18.26 17.39 19.84 9.15C19.85 9.1 20 8.84 20.2 9.09C20.73 9.77 21.57 11.92 20.54 14.85C17.99 22.08 11 20.93 10.21 20.77C10.17 20.77 10.13 20.8 10.14 20.83C10.15 21.14 10.18 22.32 9.83 22.36C9.49 22.4 4.96 18.39 5.04 18.05C5.14 17.64 10.17 15.49 10.43 15.59Z"/></g>`,
|
|
2298
|
-
turbo1: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62Z"/></g>`,
|
|
2299
|
-
autoplay: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.97 12.41C20.22 11.94 20.95 11.88 20.97 12.47C21.14 16.4 18.71 19.92 14.44 20.97C8.59 22.4 5.29 18.55 5.28 18.52C5.2 18.33 4.41 19.77 4.19 19.59C3.81 19.28 3.78 14.9 3.9 14.78C4.01 14.67 8.39 14.95 8.48 15.31C8.58 15.64 7.2 16.1 7.4 16.38C9.14 18.67 15.82 20.29 19.97 12.41Z"/><path d="M9.3 15.36L9.3 8.67C9.3 8.32 9.97 7.74 10.58 8.14C15.83 11.56 16 11.46 16.01 12.04C16.02 12.61 15.12 12.98 10.63 15.91C10.32 16.11 9.29 15.72 9.29 15.36Z"/><path d="M4.03 11.59C3.78 12.06 3.05 12.12 3.03 11.53C2.86 7.6 5.29 4.08 9.56 3.03C15.41 1.6 18.71 5.45 18.72 5.48C18.8 5.67 19.59 4.23 19.81 4.41C20.19 4.72 20.22 9.1 20.1 9.22C19.99 9.33 15.61 9.05 15.52 8.69C15.42 8.36 16.8 7.9 16.6 7.62C14.86 5.33 8.18 3.71 4.03 11.59Z"/></g>`,
|
|
2641
|
+
spin: `<g fill="currentColor" fill-rule="evenodd"><path d="M13.57 8.41C13.3 8.31 13.51 7.09 13.59 6.65C13.61 6.56 13.55 6.48 13.46 6.48C12.32 6.45 5.74 6.61 4.16 14.85C4.15 14.9 4 15.16 3.8 14.91C3.27 14.23 2.43 12.08 3.46 9.16C6.01 1.92 13 3.07 13.79 3.23C13.83 3.23 13.86 3.2 13.86 3.17C13.85 2.86 13.82 1.68 14.17 1.64C14.51 1.6 19.04 5.61 18.96 5.95C18.86 6.36 13.83 8.51 13.57 8.41L13.57 8.41Z"/><path d="M10.43 15.59C10.7 15.69 10.49 16.91 10.41 17.35C10.39 17.44 10.45 17.52 10.54 17.52C11.68 17.55 18.26 17.39 19.84 9.15C19.85 9.1 20 8.84 20.2 9.09C20.73 9.77 21.57 11.92 20.54 14.85C17.99 22.08 11 20.93 10.21 20.77C10.17 20.77 10.13 20.8 10.14 20.83C10.15 21.14 10.18 22.32 9.83 22.36C9.49 22.4 4.96 18.39 5.04 18.05C5.14 17.64 10.17 15.49 10.43 15.59L10.43 15.59L10.43 15.59Z"/></g>`,
|
|
2642
|
+
turbo1: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62L17.81 2.62Z"/></g>`,
|
|
2643
|
+
autoplay: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.97 12.41C20.22 11.94 20.95 11.88 20.97 12.47C21.14 16.4 18.71 19.92 14.44 20.97C8.59 22.4 5.29 18.55 5.28 18.52C5.2 18.33 4.41 19.77 4.19 19.59C3.81 19.28 3.78 14.9 3.9 14.78C4.01 14.67 8.39 14.95 8.48 15.31C8.58 15.64 7.2 16.1 7.4 16.38C9.14 18.67 15.82 20.29 19.97 12.41L19.97 12.41Z"/><path d="M9.3 15.36L9.3 8.67C9.3 8.32 9.97 7.74 10.58 8.14C15.83 11.56 16 11.46 16.01 12.04C16.02 12.61 15.12 12.98 10.63 15.91C10.32 16.11 9.29 15.72 9.29 15.36L9.3 15.36Z"/><path d="M4.03 11.59C3.78 12.06 3.05 12.12 3.03 11.53C2.86 7.6 5.29 4.08 9.56 3.03C15.41 1.6 18.71 5.45 18.72 5.48C18.8 5.67 19.59 4.23 19.81 4.41C20.19 4.72 20.22 9.1 20.1 9.22C19.99 9.33 15.61 9.05 15.52 8.69C15.42 8.36 16.8 7.9 16.6 7.62C14.86 5.33 8.18 3.71 4.03 11.59L4.03 11.59Z"/></g>`,
|
|
2300
2644
|
stop: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="1.6" width="20.8" height="20.8" rx="2.9"/></g>`,
|
|
2301
2645
|
menu: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.61" y="3.81" width="20.79" height="4.47" rx="2.23"/><rect x="1.6" y="9.77" width="20.79" height="4.46" rx="2.23"/><rect x="1.6" y="15.72" width="20.79" height="4.47" rx="2.23"/></g>`,
|
|
2302
2646
|
minus: `<g fill="currentColor" fill-rule="evenodd"><rect x="1.6" y="9.86" width="20.8" height="4.28" rx="2.11"/></g>`,
|
|
2303
|
-
plus: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.72 14.37L13.84 14.37L13.84 20.33C13.84 21.48 12.89 22.4 11.74 22.36C10.61 22.36 9.7 21.45 9.7 20.33L9.7 14.37L3.67 14.37C2.52 14.37 1.6 13.42 1.63 12.27C1.63 11.15 2.55 10.23 3.67 10.23L9.7 10.23L9.7 4.28C9.71 1.61 13.83 1.6 13.84 4.28L13.84 10.23L19.72 10.23C22.39 10.24 22.4 14.36 19.72 14.37Z"/></g>`,
|
|
2647
|
+
plus: `<g fill="currentColor" fill-rule="evenodd"><path d="M19.72 14.37L13.84 14.37L13.84 20.33C13.84 21.48 12.89 22.4 11.74 22.36C10.61 22.36 9.7 21.45 9.7 20.33L9.7 14.37L3.67 14.37C2.52 14.37 1.6 13.42 1.63 12.27C1.63 11.15 2.55 10.23 3.67 10.23L9.7 10.23L9.7 4.28C9.71 1.61 13.83 1.6 13.84 4.28L13.84 10.23L19.72 10.23C22.39 10.24 22.4 14.36 19.72 14.37L19.72 14.37Z"/></g>`,
|
|
2304
2648
|
gift: `<rect x="4" y="9" width="16" height="11" rx="2" fill="currentColor"/><path d="M9 9c-1.35 -0.31 -2.18 -1.65 -1.87 -3s1.65 -2.18 3 -1.87c0.93 0.22 1.65 0.94 1.87 1.87c0.31 -1.35 1.65 -2.18 3 -1.87c1.35 0.31 2.18 1.65 1.87 3c-0.22 0.93 -0.94 1.65 -1.87 1.87h-6Z" fill="currentColor"/><rect x="11" y="9" width="2" height="11" fill="rgba(0,0,0,.35)"/>`,
|
|
2305
|
-
info: `<g fill="currentColor" fill-rule="evenodd"><circle cx="11.98" cy="4.27" r="2.67"/><path d="M16.05 21.09L16.05 21.54C16.05 22.02 15.67 22.4 15.19 22.4L8.89 22.4C8.42 22.4 8.04 22.02 8.04 21.54L8.04 20.98C8.04 20.5 8.42 20.12 8.89 20.12L8.89 20.12C9.13 20.12 9.32 19.93 9.32 19.7L9.32 11.59C9.32 11.29 9.08 11.05 8.8 11.05C8.33 11.05 7.95 10.67 7.95 10.19L7.95 9.18C7.95 8.7 8.33 8.32 8.8 8.
|
|
2306
|
-
soundOn: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 15.04L1.6 9.18C1.6 8.75 1.95 8.4 2.38 8.4L5.03 8.4C5.21 8.4 5.39 8.36 5.56 8.28L11.6 5.26C12.12 5 12.73 5.38 12.73 5.96L12.73 18.27C12.73 18.85 12.12 19.22 11.6 18.96L5.56 15.94C5.39 15.86 5.22 15.82 5.03 15.82L2.38 15.82C1.95 15.82 1.6 15.47 1.6 15.04Z"/><path d="M16.01 12.11C16.04 12.36 15.67 14.7 14.7 15.23C13.92 15.42 13.54 14.77 13.86 14.1C14.89 12.56 14.88 11.66 13.86 10.13C13.54 9.46 13.92 8.8 14.7 8.99C15.67 9.53 16.06 11.81 16.01 12.11Z"/><path d="M19.15 12.12C19.2 12.51 18.6 16.4 17.3 17.05C16.32 17.54 15.99 16.87 16.42 15.81C17.8 13.4 17.59 9.93 16.42 8.41C15.73 7.5 16.34 6.58 17.26 7.18C18.74 8.15 19.21 11.64 19.15 12.12Z"/><path d="M22.4 12.16C22.16 15.18 21 17.95 19.71 18.82C18.73 19.48 18.22 18.42 18.93 17.32C20.96 14.17 21.92 9.95 18.9 6.29C18.13 5.35 19.35 4.52 20.11 5.48C21.43 7.14 22.33 9.14 22.4 12.16Z"/></g>`,
|
|
2649
|
+
info: `<g fill="currentColor" fill-rule="evenodd"><circle cx="11.98" cy="4.27" r="2.67"/><path d="M16.05 21.09L16.05 21.54C16.05 22.02 15.67 22.4 15.19 22.4L8.89 22.4C8.42 22.4 8.04 22.02 8.04 21.54L8.04 20.98C8.04 20.5 8.42 20.12 8.89 20.12L8.89 20.12C9.13 20.12 9.32 19.93 9.32 19.7L9.32 11.59C9.32 11.29 9.08 11.05 8.8 11.05C8.33 11.05 7.95 10.67 7.95 10.19L7.95 9.18C7.95 8.7 8.33 8.32 8.8 8.32L13.8 8.32C14.27 8.32 14.65 8.71 14.65 9.18L14.65 19.7C14.65 19.93 14.84 20.12 15.07 20.12L15.07 20.12C15.61 20.12 16.05 20.56 16.05 21.09L16.05 21.09Z"/></g>`,
|
|
2650
|
+
soundOn: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 15.04L1.6 9.18C1.6 8.75 1.95 8.4 2.38 8.4L5.03 8.4C5.21 8.4 5.39 8.36 5.56 8.28L11.6 5.26C12.12 5 12.73 5.38 12.73 5.96L12.73 18.27C12.73 18.85 12.12 19.22 11.6 18.96L5.56 15.94C5.39 15.86 5.22 15.82 5.03 15.82L2.38 15.82C1.95 15.82 1.6 15.47 1.6 15.04L1.6 15.04Z"/><path d="M16.01 12.11C16.04 12.36 15.67 14.7 14.7 15.23C13.92 15.42 13.54 14.77 13.86 14.1C14.89 12.56 14.88 11.66 13.86 10.13C13.54 9.46 13.92 8.8 14.7 8.99C15.67 9.53 16.06 11.81 16.01 12.11Z"/><path d="M19.15 12.12C19.2 12.51 18.6 16.4 17.3 17.05C16.32 17.54 15.99 16.87 16.42 15.81C17.8 13.4 17.59 9.93 16.42 8.41C15.73 7.5 16.34 6.58 17.26 7.18C18.74 8.15 19.21 11.64 19.15 12.12L19.15 12.12Z"/><path d="M22.4 12.16C22.16 15.18 21 17.95 19.71 18.82C18.73 19.48 18.22 18.42 18.93 17.32C20.96 14.17 21.92 9.95 18.9 6.29C18.13 5.35 19.35 4.52 20.11 5.48C21.43 7.14 22.33 9.14 22.4 12.16L22.4 12.16Z"/></g>`,
|
|
2307
2651
|
soundOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M1.6 14.77L1.6 9.23C1.6 8.82 1.93 8.49 2.34 8.49L4.85 8.49C5.02 8.49 5.19 8.45 5.34 8.38L11.06 5.52C11.55 5.27 12.12 5.63 12.12 6.18L12.12 17.82C12.12 18.37 11.55 18.73 11.06 18.48L5.34 15.62C5.19 15.55 5.02 15.51 4.85 15.51L2.34 15.51C1.93 15.51 1.6 15.18 1.6 14.77Z"/><path d="M21.95 16.53C21.5 16.98 20.78 16.98 20.33 16.53L17.79 13.99L15.25 16.54C14.8 16.99 14.07 16.99 13.62 16.54C13.17 16.09 13.17 15.36 13.62 14.92L16.17 12.37L13.63 9.83C13.18 9.38 13.18 8.65 13.63 8.21C13.85 7.98 14.14 7.87 14.44 7.87C14.73 7.87 15.02 7.98 15.25 8.21L17.79 10.75L20.33 8.21C20.77 7.76 21.5 7.76 21.95 8.21C22.17 8.44 22.28 8.73 22.28 9.02C22.28 9.32 22.17 9.61 21.95 9.83L19.41 12.37L21.95 14.91C22.4 15.36 22.4 16.08 21.95 16.53Z"/></g>`,
|
|
2308
|
-
close: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.39 21.37C20.38 22.38 18.74 22.38 17.73 21.37L12.01 15.64L6.27 21.38C5.26 22.39 3.62 22.39 2.61 21.38C1.6 20.38 1.6 18.74 2.61 17.73L8.35 11.99L2.62 6.25C1.61 5.24 1.61 3.61 2.62 2.6C3.12 2.09 3.79 1.84 4.45 1.84C5.11 1.84 5.77 2.09 6.28 2.6L12.01 8.33L17.72 2.62C18.73 1.61 20.37 1.61 21.38 2.62C21.89 3.12 22.14 3.78 22.14 4.44C22.14 5.11 21.89 5.77 21.38 6.27L15.67 11.99L21.39 17.71C22.4 18.72 22.4 20.36 21.39 21.37Z"/></g>`,
|
|
2309
|
-
back: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.49 2.32L17.49 2.32C18.2 3.03 18.2 4.19 17.49 4.9L11.68 10.71C10.97 11.42 10.97 12.58 11.68 13.29L17.49 19.1C18.21 19.81 18.21 20.97 17.49 21.69L17.49 21.69C16.78 22.4 15.62 22.4 14.91 21.69L7.8 14.58L7.8 14.
|
|
2310
|
-
chevronRight: `<g fill="currentColor" fill-rule="evenodd"><path d="M6.51 21.68L6.51 21.68C5.8 20.97 5.8 19.81 6.51 19.1L12.32 13.29C13.03 12.58 13.03 11.42 12.32 10.71L6.51 4.9C5.79 4.19 5.79 3.03 6.51 2.31L6.51 2.31C7.22 1.6 8.38 1.6 9.09 2.31L16.2 9.42L16.2 9.
|
|
2652
|
+
close: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.39 21.37C20.38 22.38 18.74 22.38 17.73 21.37L12.01 15.64L6.27 21.38C5.26 22.39 3.62 22.39 2.61 21.38C1.6 20.38 1.6 18.74 2.61 17.73L8.35 11.99L2.62 6.25C1.61 5.24 1.61 3.61 2.62 2.6C3.12 2.09 3.79 1.84 4.45 1.84C5.11 1.84 5.77 2.09 6.28 2.6L12.01 8.33L17.72 2.62C18.73 1.61 20.37 1.61 21.38 2.62C21.89 3.12 22.14 3.78 22.14 4.44C22.14 5.11 21.89 5.77 21.38 6.27L15.67 11.99L21.39 17.71C22.4 18.72 22.4 20.36 21.39 21.37L21.39 21.37Z"/></g>`,
|
|
2653
|
+
back: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.49 2.32L17.49 2.32C18.2 3.03 18.2 4.19 17.49 4.9L11.68 10.71C10.97 11.42 10.97 12.58 11.68 13.29L17.49 19.1C18.21 19.81 18.21 20.97 17.49 21.69L17.49 21.69C16.78 22.4 15.62 22.4 14.91 21.69L7.8 14.58L7.8 14.58L6.51 13.29C5.79 12.58 5.79 11.42 6.51 10.71L14.9 2.31C15.62 1.6 16.77 1.6 17.49 2.31L17.49 2.31Z"/></g>`,
|
|
2654
|
+
chevronRight: `<g fill="currentColor" fill-rule="evenodd"><path d="M6.51 21.68L6.51 21.68C5.8 20.97 5.8 19.81 6.51 19.1L12.32 13.29C13.03 12.58 13.03 11.42 12.32 10.71L6.51 4.9C5.79 4.19 5.79 3.03 6.51 2.31L6.51 2.31C7.22 1.6 8.38 1.6 9.09 2.31L16.2 9.42L16.2 9.42L17.49 10.71C18.21 11.42 18.21 12.58 17.49 13.29L9.1 21.69C8.38 22.4 7.23 22.4 6.51 21.69L6.51 21.69Z"/></g>`,
|
|
2311
2655
|
ticket: `<path d="M1.72 9.48L19.64 4.67L20.28 7.07C19.19 7.8 18.82 9 19.11 10.09C19.41 11.19 20.33 12.04 21.64 12.13L22.28 14.52L4.36 19.32L3.72 16.93C4.81 16.2 5.18 15 4.89 13.91C4.59 12.81 3.67 11.96 2.36 11.87L1.72 9.48Z" fill="none" stroke="currentColor" stroke-width="1.84" stroke-linejoin="miter"/><path d="M12.57 7.17L9.75 13.26L11.92 12.4L11.84 16.74L14.85 10.06L12.38 11.1Z" fill="currentColor"/>`,
|
|
2312
|
-
turbo2: `<g fill="currentColor" fill-rule="evenodd"><path d="M8.24 8.81L3.92 7.68C3.88 7.67 3.88 7.61 3.92 7.61L9.66 6.45C9.8 6.43 9.89 6.56 9.83 6.68L8.86 8.35C8.67 8.73 8.33 8.83 8.24 8.81Z"/><path d="M20.58 3.04L17.34 8.38C17.09 8.8 17.39 9.33 17.87 9.33L21.57 9.33C22.12 9.33 22.4 9.99 22.02 10.39L11.24 21.73C11.12 21.86 10.95 21.93 10.77 21.92L10.77 21.92C10.34 21.92 10.04 21.47 10.2 21.07L13.04 13.82C13.21 13.41 12.9 12.96 12.45 12.97L8.46 13.01C7.98 13.01 7.67 12.5 7.9 12.08L13.2 2.4C13.31 2.2 13.52 2.07 13.75 2.07L20.05 2.09C20.54 2.1 20.84 2.63 20.58 3.04Z"/><path d="M5.96 12.68L1.64 11.54C1.6 11.54 1.6 11.47 1.64 11.47L7.38 10.31C7.
|
|
2313
|
-
turboOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62Z"/></g>`,
|
|
2314
|
-
chevronUp: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.68 17.49L21.68 17.49C20.97 18.2 19.81 18.2 19.1 17.49L13.29 11.68C12.58 10.97 11.42 10.97 10.71 11.68L4.9 17.49C4.19 18.21 3.03 18.21 2.31 17.49L2.31 17.49C1.6 16.78 1.6 15.62 2.31 14.91L9.42 7.8L9.42 7.
|
|
2315
|
-
chevronDown: `<g fill="currentColor" fill-rule="evenodd"><path d="M2.32 6.51L2.32 6.51C3.03 5.8 4.19 5.8 4.9 6.51L10.71 12.32C11.42 13.03 12.58 13.03 13.29 12.32L19.1 6.51C19.82 5.79 20.97 5.79 21.69 6.51L21.69 6.51C22.4 7.22 22.4 8.38 21.69 9.09L14.58 16.19L14.58 16.
|
|
2656
|
+
turbo2: `<g fill="currentColor" fill-rule="evenodd"><path d="M8.24 8.81L3.92 7.68C3.88 7.67 3.88 7.61 3.92 7.61L9.66 6.45C9.8 6.43 9.89 6.56 9.83 6.68L8.86 8.35C8.67 8.73 8.33 8.83 8.24 8.81L8.24 8.81Z"/><path d="M20.58 3.04L17.34 8.38C17.09 8.8 17.39 9.33 17.87 9.33L21.57 9.33C22.12 9.33 22.4 9.99 22.02 10.39L11.24 21.73C11.12 21.86 10.95 21.93 10.77 21.92L10.77 21.92C10.34 21.92 10.04 21.47 10.2 21.07L13.04 13.82C13.21 13.41 12.9 12.96 12.45 12.97L8.46 13.01C7.98 13.01 7.67 12.5 7.9 12.08L13.2 2.4C13.31 2.2 13.52 2.07 13.75 2.07L20.05 2.09C20.54 2.1 20.84 2.63 20.58 3.04L20.58 3.04Z"/><path d="M5.96 12.68L1.64 11.54C1.6 11.54 1.6 11.47 1.64 11.47L7.38 10.31C7.51 10.3 7.61 10.42 7.55 10.55L6.58 12.22C6.39 12.59 6.05 12.69 5.96 12.68L5.96 12.68Z"/><path d="M9.67 16.66L5.35 15.53C5.31 15.52 5.31 15.46 5.35 15.46L11.09 14.3C11.22 14.28 11.32 14.41 11.26 14.53L10.29 16.2C10.1 16.58 9.76 16.68 9.67 16.66L9.67 16.66Z"/></g>`,
|
|
2657
|
+
turboOff: `<g fill="currentColor" fill-rule="evenodd"><path d="M17.81 2.62L14.41 8.21C14.15 8.64 14.46 9.2 14.97 9.2L18.84 9.21C19.42 9.21 19.72 9.9 19.32 10.31L8.02 22.19C7.89 22.33 7.72 22.4 7.53 22.4L7.53 22.4C7.08 22.39 6.77 21.93 6.93 21.5L9.91 13.91C10.08 13.48 9.76 13.01 9.29 13.01L5.11 13.06C4.61 13.06 4.28 12.53 4.53 12.09L10.07 1.94C10.19 1.73 10.41 1.6 10.65 1.6L17.26 1.62C17.77 1.62 18.08 2.18 17.81 2.62L17.81 2.62Z"/></g>`,
|
|
2658
|
+
chevronUp: `<g fill="currentColor" fill-rule="evenodd"><path d="M21.68 17.49L21.68 17.49C20.97 18.2 19.81 18.2 19.1 17.49L13.29 11.68C12.58 10.97 11.42 10.97 10.71 11.68L4.9 17.49C4.19 18.21 3.03 18.21 2.31 17.49L2.31 17.49C1.6 16.78 1.6 15.62 2.31 14.91L9.42 7.8L9.42 7.8L10.71 6.51C11.42 5.79 12.58 5.79 13.29 6.51L21.69 14.9C22.4 15.62 22.4 16.77 21.69 17.49L21.69 17.49Z"/></g>`,
|
|
2659
|
+
chevronDown: `<g fill="currentColor" fill-rule="evenodd"><path d="M2.32 6.51L2.32 6.51C3.03 5.8 4.19 5.8 4.9 6.51L10.71 12.32C11.42 13.03 12.58 13.03 13.29 12.32L19.1 6.51C19.82 5.79 20.97 5.79 21.69 6.51L21.69 6.51C22.4 7.22 22.4 8.38 21.69 9.09L14.58 16.19L14.58 16.2L13.29 17.49C12.58 18.21 11.42 18.21 10.71 17.49L2.31 9.1C1.6 8.38 1.6 7.23 2.31 6.51L2.32 6.51L2.32 6.51Z"/></g>`,
|
|
2316
2660
|
};
|
|
2317
2661
|
/** A standalone, parseable SVG document with `currentColor` resolved to `color`.
|
|
2318
2662
|
* Used by the Pixi renderer to build vector geometry via GraphicsContext.svg(). */
|
|
@@ -2348,10 +2692,10 @@ class IconView extends pixi_js.Container {
|
|
|
2348
2692
|
gfx;
|
|
2349
2693
|
_size;
|
|
2350
2694
|
_color;
|
|
2351
|
-
|
|
2695
|
+
_iconName;
|
|
2352
2696
|
constructor(name, size, color = '#ffffff') {
|
|
2353
2697
|
super();
|
|
2354
|
-
this.
|
|
2698
|
+
this._iconName = name;
|
|
2355
2699
|
this._size = size;
|
|
2356
2700
|
this._color = color;
|
|
2357
2701
|
this.gfx = new pixi_js.Graphics(context(name, color));
|
|
@@ -2367,11 +2711,22 @@ class IconView extends pixi_js.Container {
|
|
|
2367
2711
|
get size() {
|
|
2368
2712
|
return this._size;
|
|
2369
2713
|
}
|
|
2714
|
+
get iconName() {
|
|
2715
|
+
return this._iconName;
|
|
2716
|
+
}
|
|
2370
2717
|
setColor(color) {
|
|
2371
2718
|
if (color === this._color)
|
|
2372
2719
|
return;
|
|
2373
2720
|
this._color = color;
|
|
2374
|
-
this.gfx.context = context(this.
|
|
2721
|
+
this.gfx.context = context(this._iconName, color);
|
|
2722
|
+
}
|
|
2723
|
+
/** Swap the glyph in place (e.g. the menu's sound row flips soundOn/soundOff on toggle) — same
|
|
2724
|
+
* cached-context rebuild as setColor, keyed on the new name instead of a new colour. */
|
|
2725
|
+
setIcon(name) {
|
|
2726
|
+
if (name === this._iconName)
|
|
2727
|
+
return;
|
|
2728
|
+
this._iconName = name;
|
|
2729
|
+
this.gfx.context = context(name, this._color);
|
|
2375
2730
|
}
|
|
2376
2731
|
setSize(size) {
|
|
2377
2732
|
if (size === this._size)
|
|
@@ -3103,6 +3458,11 @@ class BottomBar extends pixi_js.Container {
|
|
|
3103
3458
|
spin;
|
|
3104
3459
|
autoBtn;
|
|
3105
3460
|
turboBtn;
|
|
3461
|
+
menuBtn;
|
|
3462
|
+
/** The menu's PLATE, in LOCAL coordinates relative to `inner` — the continuous dark panel (wide)
|
|
3463
|
+
* or the controls row (mobile). Set at the point `applyFit()`/`applyFitMobile()` compute it;
|
|
3464
|
+
* `menuPlate()` converts it to screen space lazily (see that method's doc comment for why). */
|
|
3465
|
+
plateRect = null;
|
|
3106
3466
|
constructor(host) {
|
|
3107
3467
|
super();
|
|
3108
3468
|
this.host = host;
|
|
@@ -3112,6 +3472,58 @@ class BottomBar extends pixi_js.Container {
|
|
|
3112
3472
|
else
|
|
3113
3473
|
this.buildWide();
|
|
3114
3474
|
}
|
|
3475
|
+
/** Screen-space rect of the burger — the menu popover's POINTER (the arrow's target only; see
|
|
3476
|
+
* `menuPlate` for what drives placement). `measureSize()` (the button's own nominal box, e.g.
|
|
3477
|
+
* 36×36) rather than the built-in `getSize()` (the glyph's ink bounds only) so a burger whose icon
|
|
3478
|
+
* doesn't fill its box still reports its true hit-box size; multiplied by `inner.scale` — `getSize`
|
|
3479
|
+
* /`measureSize` only reflect the node's OWN scale (always 1 here), not the ancestor `inner.scale`
|
|
3480
|
+
* the bar's whole fit-scale lives on, so the UNMULTIPLIED width/height would silently overstate the
|
|
3481
|
+
* burger's true on-screen footprint on a scaled-down bar and skew the arrow off-centre. */
|
|
3482
|
+
menuAnchor() {
|
|
3483
|
+
if (!this.menuBtn || this.menuBtn.destroyed)
|
|
3484
|
+
return null;
|
|
3485
|
+
const p = this.menuBtn.getGlobalPosition();
|
|
3486
|
+
const s = this.inner.scale.x;
|
|
3487
|
+
const size = this.menuBtn.measureSize();
|
|
3488
|
+
return { x: p.x, y: p.y, w: size.w * s, h: size.h * s };
|
|
3489
|
+
}
|
|
3490
|
+
/** Screen-space rect of the menu's PLATE — the continuous dark panel (wide) or the controls row
|
|
3491
|
+
* (mobile), NOT the burger itself and NOT (mobile) the info pill below it. Drives the popover's
|
|
3492
|
+
* x/y/maxH/below; the burger (`menuAnchor`) drives only the arrow. Resolved lazily on every call,
|
|
3493
|
+
* like `menuAnchor` — `PixiRenderer.renderBar()` destroys and rebuilds this BottomBar on every
|
|
3494
|
+
* resize and ~20 other state changes, so a value captured once would go stale. `plateRect` is
|
|
3495
|
+
* local to `inner`; `getGlobalPosition` + `inner.scale` convert it to screen space (`outer*` on the
|
|
3496
|
+
* mobile FlexBox and the wide panel's own drawn rect are both LOCAL sizes, unaffected by `inner`'s
|
|
3497
|
+
* ancestor scale, same reasoning as `menuAnchor` above). */
|
|
3498
|
+
menuPlate() {
|
|
3499
|
+
// Pixi v8's Container.destroy() nulls `_position`/`_scale`, so `inner.scale.x` /
|
|
3500
|
+
// `inner.getGlobalPosition()` on a destroyed bar throws — guard for symmetry with menuAnchor()
|
|
3501
|
+
// above, which already returns null once its button is destroyed. Not reachable today
|
|
3502
|
+
// (PixiRenderer.renderBar() destroys and reassigns `this.bar` synchronously, in the same call),
|
|
3503
|
+
// but the asymmetry is a trap for a future caller that holds a reference across a render.
|
|
3504
|
+
if (this.destroyed)
|
|
3505
|
+
return null;
|
|
3506
|
+
if (!this.plateRect)
|
|
3507
|
+
return null;
|
|
3508
|
+
const s = this.inner.scale.x;
|
|
3509
|
+
const origin = this.inner.getGlobalPosition();
|
|
3510
|
+
return {
|
|
3511
|
+
x: origin.x + this.plateRect.x * s,
|
|
3512
|
+
y: origin.y + this.plateRect.y * s,
|
|
3513
|
+
w: this.plateRect.w * s,
|
|
3514
|
+
h: this.plateRect.h * s,
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
/** The scale factor the bar currently applies to itself (`inner.scale`) — shared with the menu
|
|
3518
|
+
* popover so its typography/padding/row-heights scale in lockstep with the bar's own chrome,
|
|
3519
|
+
* instead of ignoring the viewport like a fixed-local-size card would. */
|
|
3520
|
+
fitScale() {
|
|
3521
|
+
// Same destroyed guard as menuPlate()/menuAnchor() above, and the same neutral fallback a
|
|
3522
|
+
// scale factor should have (1 = no scaling) rather than throwing.
|
|
3523
|
+
if (this.destroyed)
|
|
3524
|
+
return 1;
|
|
3525
|
+
return this.inner.scale.x;
|
|
3526
|
+
}
|
|
3115
3527
|
// ── wide / landscape ──────────────────────────────────────────────────────
|
|
3116
3528
|
buildWide() {
|
|
3117
3529
|
const { state, tokens } = this.host;
|
|
@@ -3122,13 +3534,14 @@ class BottomBar extends pixi_js.Container {
|
|
|
3122
3534
|
this.buy = isBase ? (this.buildBuy(BUY_W, 13, 3) ?? undefined) : undefined;
|
|
3123
3535
|
// LEFT info group: menu · balance · (Total win) · (Win)
|
|
3124
3536
|
const left = new FlexBox({ direction: 'row', align: 'center', gap: ZONE_GAP });
|
|
3125
|
-
|
|
3537
|
+
this.menuBtn = new IconButton('menu', {
|
|
3126
3538
|
size: 36,
|
|
3127
3539
|
glyph: 30,
|
|
3128
3540
|
color: '#ffffff',
|
|
3129
3541
|
hover: tokens.accent,
|
|
3130
3542
|
onTap: () => this.host.actions.openMenu(),
|
|
3131
|
-
})
|
|
3543
|
+
});
|
|
3544
|
+
left.add(this.menuBtn);
|
|
3132
3545
|
if (!state.replay) {
|
|
3133
3546
|
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance));
|
|
3134
3547
|
this.balanceValue = bal.valueText;
|
|
@@ -3317,6 +3730,7 @@ class BottomBar extends pixi_js.Container {
|
|
|
3317
3730
|
hover: tokens.accent,
|
|
3318
3731
|
onTap: () => this.host.actions.openMenu(),
|
|
3319
3732
|
});
|
|
3733
|
+
this.menuBtn = menu;
|
|
3320
3734
|
// autoplay is a base-mode control only — hidden in free spins / replay (matches the DOM bar)
|
|
3321
3735
|
if (isBase && config.features.autoplay) {
|
|
3322
3736
|
this.autoBtn = new IconButton('autoplay', {
|
|
@@ -3552,6 +3966,8 @@ class BottomBar extends pixi_js.Container {
|
|
|
3552
3966
|
this.panelBg.clear();
|
|
3553
3967
|
roundedPath(this.panelBg, panelX, SPIN_POP, panelRight - panelX, BAR_H, [12, 12, 12, 12]);
|
|
3554
3968
|
this.panelBg.fill(tokens.bar);
|
|
3969
|
+
// The menu's plate — the whole continuous dark panel, local to `inner` (see menuPlate()).
|
|
3970
|
+
this.plateRect = { x: panelX, y: SPIN_POP, w: panelRight - panelX, h: BAR_H };
|
|
3555
3971
|
if (this.buy)
|
|
3556
3972
|
this.buy.position.set(OUTER_PAD, panelCenterY - BUY_W / 2);
|
|
3557
3973
|
left.position.set(panelX + PANEL_PAD, panelCenterY - left.outerHeight / 2);
|
|
@@ -3584,6 +4000,18 @@ class BottomBar extends pixi_js.Container {
|
|
|
3584
4000
|
// SPIN hero), inflating the controls row so it overlaps the info row below it.
|
|
3585
4001
|
controls.setLayoutSize(rowW, M_CTRL_H);
|
|
3586
4002
|
info.setLayoutSize(rowW, M_INFO_H);
|
|
4003
|
+
// The menu's plate — the controls row (NOT the info pill below it), local to `inner`. Read from
|
|
4004
|
+
// `controls.outerWidth/outerHeight` (its own nominal box), NOT the FlexBox's own bounds (which
|
|
4005
|
+
// would include the SPIN/FS hero popping out both above AND below it — see menuPlate()'s doc
|
|
4006
|
+
// comment) — but the TOP edge is deliberately extended upward by `pop`, the same hero pop-out
|
|
4007
|
+
// amount reserved as `topPad` above, so the plate the popover math sees is the row's true visual
|
|
4008
|
+
// extent on the side that matters: without this, the card's bottom (only `gap` above the plate's
|
|
4009
|
+
// top) can clip the top ~(pop-gap)px of the hero's arc, since 62px M_CTRL_H is 11px shorter than
|
|
4010
|
+
// the 84px hero. The BOTTOM edge is left alone — the hero's symmetric under-pop is a separate,
|
|
4011
|
+
// unrelated concern (the info pill sits right below with its own small gap) and extending it too
|
|
4012
|
+
// would perturb the `below`-placement branch (spaceBelow) for no benefit. A no-op when `pop` is 0
|
|
4013
|
+
// (no hero shown — e.g. replay without free spins).
|
|
4014
|
+
this.plateRect = { x: 0, y: topPad - pop, w: controls.outerWidth, h: controls.outerHeight + pop };
|
|
3587
4015
|
const s = rowW > 0 ? Math.max(0.4, Math.min(1, avail / rowW)) : 1;
|
|
3588
4016
|
this.inner.scale.set(s);
|
|
3589
4017
|
this.inner.position.set((W - rowW * s) / 2, H - localBottom * s - M_PAD_BOTTOM);
|
|
@@ -3614,7 +4042,8 @@ class ScrollBox extends pixi_js.Container {
|
|
|
3614
4042
|
this.canvas = canvas;
|
|
3615
4043
|
this.addChild(this.content);
|
|
3616
4044
|
// maskG is added to the scene only while scrolling (see refresh) — a leftover unused mask
|
|
3617
|
-
// graphic renders as a white rect
|
|
4045
|
+
// graphic renders as a white rect. (Masking does NOT gate pointer events to the content, despite
|
|
4046
|
+
// what an earlier version of this comment claimed — see the correction in refresh() below.)
|
|
3618
4047
|
this.eventMode = 'static';
|
|
3619
4048
|
this.on('pointerdown', this.onDown);
|
|
3620
4049
|
this.on('globalpointermove', this.onMove);
|
|
@@ -3648,10 +4077,13 @@ class ScrollBox extends pixi_js.Container {
|
|
|
3648
4077
|
const b = this.content.getLocalBounds();
|
|
3649
4078
|
const contentH = b.height + b.y; // content laid out from y≈0 downward
|
|
3650
4079
|
this.maxScroll = Math.max(0, contentH - this.viewH);
|
|
3651
|
-
// Only clip + grab pointer/drag when the content actually overflows
|
|
3652
|
-
// pointer events to its children in Pixi v8
|
|
3653
|
-
//
|
|
3654
|
-
//
|
|
4080
|
+
// Only clip + grab pointer/drag when the content actually overflows. (An earlier version of this
|
|
4081
|
+
// comment claimed a masked container blocks pointer events to its children in Pixi v8 — that's
|
|
4082
|
+
// false: EventBoundary's hitPruneFn only prunes a point OUTSIDE the mask/hitArea, so interactive
|
|
4083
|
+
// children stay reachable anywhere inside the visible viewport; see tests/pixi/bet-picker-fit.test.ts
|
|
4084
|
+
// and tests/pixi/menu.test.ts, which hit-test an overflowing, masked list on purpose to prove it.
|
|
4085
|
+
// We still only mask + grab the pointer when scrollable, since an unmasked, passive box is
|
|
4086
|
+
// simpler and cheaper when there's nothing to clip or drag.)
|
|
3655
4087
|
const scrollable = this.maxScroll > 0;
|
|
3656
4088
|
if (scrollable) {
|
|
3657
4089
|
this.addChild(this.maskG);
|
|
@@ -3711,152 +4143,135 @@ class ScrollBox extends pixi_js.Container {
|
|
|
3711
4143
|
}
|
|
3712
4144
|
}
|
|
3713
4145
|
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
/** Small rounded icon nav button — the overlay header back/close and row chevrons.
|
|
3718
|
-
* `.ge-ov-nav`: 32×32, radius 9, plaque-dark bg, white icon, hover → glass bg + accent icon. */
|
|
3719
|
-
function navButton(host, iconName, onTap, size = 32) {
|
|
3720
|
-
const root = new pixi_js.Container();
|
|
3721
|
-
const bg = new pixi_js.Graphics();
|
|
3722
|
-
const draw = (hover) => {
|
|
3723
|
-
bg.clear();
|
|
3724
|
-
bg.roundRect(0, 0, size, size, 9);
|
|
3725
|
-
bg.fill(hover ? host.tokens.plaqueGlass : host.tokens.plaqueDark);
|
|
3726
|
-
};
|
|
3727
|
-
draw(false);
|
|
3728
|
-
const gs = Math.round(size * 0.5625); // glyph tracks the box (default 32 → 18, unchanged)
|
|
3729
|
-
const glyph = makeIcon(iconName, gs, '#ffffff');
|
|
3730
|
-
glyph.position.set((size - gs) / 2, (size - gs) / 2);
|
|
3731
|
-
root.addChild(bg, glyph);
|
|
3732
|
-
root.eventMode = 'static';
|
|
3733
|
-
root.cursor = 'pointer';
|
|
3734
|
-
root.hitArea = new pixi_js.Rectangle(0, 0, size, size);
|
|
3735
|
-
attachHover(root, () => {
|
|
3736
|
-
draw(true);
|
|
3737
|
-
glyph.setColor(host.tokens.accent);
|
|
3738
|
-
}, () => {
|
|
3739
|
-
draw(false);
|
|
3740
|
-
glyph.setColor('#ffffff');
|
|
3741
|
-
});
|
|
3742
|
-
attachPress(root, 0.92, onTap);
|
|
3743
|
-
return root;
|
|
3744
|
-
}
|
|
3745
|
-
/** Full-screen overlay: frosted dark veil, fixed header (title + back/close), scrolling body.
|
|
3746
|
-
* The body is rebuilt on resize so it reflows like the CSS overlay. */
|
|
3747
|
-
class Overlay extends pixi_js.Container {
|
|
4146
|
+
/** Light-dismiss popover: a transparent full-screen hit rect + a rounded card with an arrow.
|
|
4147
|
+
* No veil, no frosted snapshot — the game stays visible and unblurred behind it. */
|
|
4148
|
+
class Popover extends pixi_js.Container {
|
|
3748
4149
|
tag;
|
|
4150
|
+
dismissLayer = new pixi_js.Graphics();
|
|
4151
|
+
card = new pixi_js.Container();
|
|
4152
|
+
bg = new pixi_js.Graphics();
|
|
4153
|
+
arrow = new pixi_js.Graphics();
|
|
4154
|
+
scroll;
|
|
3749
4155
|
host;
|
|
3750
4156
|
opts;
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
nav = new pixi_js.Container();
|
|
3756
|
-
w = 0;
|
|
3757
|
-
h = 0;
|
|
3758
|
-
headerH = 44;
|
|
3759
|
-
/** Expose the scroll box for keyboard/test access. */
|
|
3760
|
-
get scrollContent() { return this.scroll; }
|
|
4157
|
+
_cardX = 0;
|
|
4158
|
+
_cardY = 0;
|
|
4159
|
+
_cardW = 0;
|
|
4160
|
+
_arrowX = -1;
|
|
3761
4161
|
constructor(host, opts) {
|
|
3762
4162
|
super();
|
|
3763
4163
|
this.host = host;
|
|
3764
4164
|
this.opts = opts;
|
|
3765
4165
|
this.tag = opts.tag;
|
|
3766
4166
|
this.scroll = new ScrollBox(host.canvas);
|
|
3767
|
-
this.addChild(this.
|
|
3768
|
-
this.
|
|
4167
|
+
this.card.addChild(this.bg, this.arrow, this.scroll);
|
|
4168
|
+
this.addChild(this.dismissLayer, this.card);
|
|
4169
|
+
this.dismissLayer.eventMode = 'static';
|
|
4170
|
+
this.dismissLayer.on('pointertap', () => this.opts.onClose());
|
|
4171
|
+
// Taps on the card must not fall through to the dismiss layer.
|
|
4172
|
+
this.card.eventMode = 'static';
|
|
4173
|
+
// `e` itself is undefined when a caller (or test) emits the event with no payload, so guard the
|
|
4174
|
+
// property access, not just the call.
|
|
4175
|
+
this.card.on('pointertap', (e) => e?.stopPropagation?.());
|
|
3769
4176
|
this.resize(host.screenW, host.screenH);
|
|
3770
4177
|
}
|
|
4178
|
+
get cardX() { return this._cardX; }
|
|
4179
|
+
get cardY() { return this._cardY; }
|
|
4180
|
+
get cardWidth() { return this._cardW; }
|
|
4181
|
+
get arrowX() { return this._arrowX; }
|
|
4182
|
+
get arrowVisible() { return this.arrow.visible; }
|
|
3771
4183
|
resize(w, h) {
|
|
3772
|
-
this.
|
|
3773
|
-
this.h
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
this
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
//
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
const
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
title.position.set(this.w / 2, this.headerH / 2 + pad / 2);
|
|
3803
|
-
left.position.set(pad, (this.headerH - 32) / 2 + pad / 2);
|
|
3804
|
-
close.position.set(this.w - 32 - pad, (this.headerH - 32) / 2 + pad / 2);
|
|
3805
|
-
this.header.addChild(title, left, close);
|
|
3806
|
-
}
|
|
3807
|
-
layoutBody() {
|
|
3808
|
-
const top = this.headerH + 6;
|
|
3809
|
-
const sidePad = clamp(16, 4 * (this.w / 100), 24);
|
|
3810
|
-
const vPad = clamp(6, 2 * (this.h / 100), 16);
|
|
3811
|
-
const bodyW = Math.min(800, this.w - sidePad * 2);
|
|
3812
|
-
this.scroll.position.set(0, top);
|
|
3813
|
-
this.scroll.setViewport(this.w, this.h - top);
|
|
3814
|
-
this.scroll.content.removeChildren().forEach((c) => c.destroy({ children: true }));
|
|
3815
|
-
const content = this.opts.build(bodyW);
|
|
4184
|
+
this.dismissLayer.clear();
|
|
4185
|
+
this.dismissLayer.rect(0, 0, w, h).fill({ color: 0x000000, alpha: 0 });
|
|
4186
|
+
this.dismissLayer.hitArea = new pixi_js.Rectangle(0, 0, w, h);
|
|
4187
|
+
const s = this.opts.scale?.() ?? 1;
|
|
4188
|
+
const pad = 8;
|
|
4189
|
+
// Width is content-driven (spec: clamped to [220, min(320, surfaceWidth-16)]), so the real
|
|
4190
|
+
// content has to be measured before we know the final width to lay it out at. Probe-build once
|
|
4191
|
+
// at the SMALLEST allowed inner width purely to measure natural size, then throw that copy away
|
|
4192
|
+
// and build the kept one at the resolved final width. Measured in LOCAL (unscaled) units, like
|
|
4193
|
+
// every other Pixi size in this file — `s` only converts to screen units where placement needs
|
|
4194
|
+
// it (the card carries the scale as a single transform, same as the DOM's card).
|
|
4195
|
+
//
|
|
4196
|
+
// The probe deliberately measures at the MINIMUM, not the maximum: a decorative row (the menu's
|
|
4197
|
+
// separator) draws its divider line at exactly the width `build()` is called with — it has no
|
|
4198
|
+
// content-driven size of its own, unlike every real row, which ignores that parameter and sizes
|
|
4199
|
+
// from its icon/label/control regardless. Probing at the maximum would make that separator line
|
|
4200
|
+
// alone measure near-maximum and dominate `naturalWidth`, pegging the card at ~maxW for every
|
|
4201
|
+
// menu that has a separator — which the default menu always does. Probing at the minimum cannot
|
|
4202
|
+
// distort the result the other way: content narrower than the probe still clamps to minW either
|
|
4203
|
+
// way (nothing above changes), and content wider than the probe always wins the max() the layout
|
|
4204
|
+
// takes over row widths, so real content still drives growth past minW correctly.
|
|
4205
|
+
const probeW = POPOVER.minW - pad * 2;
|
|
4206
|
+
const probe = this.opts.build(probeW);
|
|
4207
|
+
const measured = probe instanceof FlexBox ? probe.measureSize().w : probe.getSize().width;
|
|
4208
|
+
probe.destroy({ children: true });
|
|
4209
|
+
// Resolve the ON-SCREEN width (screen units, clamped against the surface), then convert back to
|
|
4210
|
+
// LOCAL for the actual content layout — mirrors the DOM's naturalW·s → resolvedW → style.width÷s.
|
|
4211
|
+
const screenW = popoverWidth(w, (measured + pad * 2) * s);
|
|
4212
|
+
const localW = s > 0 ? screenW / s : screenW;
|
|
4213
|
+
const content = this.opts.build(localW - pad * 2);
|
|
3816
4214
|
if (content instanceof FlexBox)
|
|
3817
|
-
content.setLayoutSize(
|
|
3818
|
-
content.
|
|
4215
|
+
content.setLayoutSize(localW - pad * 2, undefined);
|
|
4216
|
+
const contentH = content.getSize().height; // local units
|
|
4217
|
+
// The plate falls back to the pointer when it can't be resolved (no distinct plaque), and to the
|
|
4218
|
+
// centred/arrow-less layout when neither resolves — placePopover itself already defaults the
|
|
4219
|
+
// ARROW to `pointer ?? plate`, so passing the pointer through unconditionally is enough there.
|
|
4220
|
+
// A DEGENERATE (fully zero-sized) plate counts as unresolved too, matching the DOM renderer's
|
|
4221
|
+
// rectOf(): `??` only falls through on null/undefined, so a real-but-{w:0,h:0} rect (e.g. read
|
|
4222
|
+
// before the bar's first layout pass) would otherwise silently win over a perfectly good pointer.
|
|
4223
|
+
const pointerRect = this.opts.pointer?.() ?? null;
|
|
4224
|
+
const rawPlate = this.opts.plate();
|
|
4225
|
+
const plateRect = rawPlate && !(rawPlate.w <= 0 && rawPlate.h <= 0) ? rawPlate : pointerRect;
|
|
4226
|
+
const p = placePopover(plateRect, { w, h }, { w: screenW, h: (contentH + pad * 2) * s }, pointerRect);
|
|
4227
|
+
const maxHLocal = s > 0 ? p.maxH / s : p.maxH;
|
|
4228
|
+
const cardH = Math.min(contentH + pad * 2, maxHLocal); // local units
|
|
4229
|
+
this.scroll.content.removeChildren().forEach((c) => c.destroy({ children: true }));
|
|
4230
|
+
this.scroll.position.set(pad, pad);
|
|
4231
|
+
this.scroll.setViewport(localW - pad * 2, cardH - pad * 2);
|
|
3819
4232
|
this.scroll.content.addChild(content);
|
|
3820
4233
|
this.scroll.refresh();
|
|
3821
|
-
|
|
3822
|
-
|
|
4234
|
+
this.bg.clear();
|
|
4235
|
+
this.bg.roundRect(0, 0, localW, cardH, 18);
|
|
4236
|
+
this.bg.fill(this.host.tokens.plaqueDark);
|
|
4237
|
+
// Arrow: a 14×7 triangle (local units — it lives inside the scaled card) on the edge that faces
|
|
4238
|
+
// the plate.
|
|
4239
|
+
this.arrow.clear();
|
|
4240
|
+
this.arrow.visible = p.arrowX >= 0;
|
|
4241
|
+
if (this.arrow.visible) {
|
|
4242
|
+
const arrowXLocal = s > 0 ? p.arrowX / s : p.arrowX;
|
|
4243
|
+
const edge = p.below ? 0 : cardH; // the card edge the arrow sits on
|
|
4244
|
+
const tip = p.below ? -7 : cardH + 7; // the tip, pointing at the pointer (or the plate)
|
|
4245
|
+
this.arrow.moveTo(arrowXLocal - 7, edge);
|
|
4246
|
+
this.arrow.lineTo(arrowXLocal + 7, edge);
|
|
4247
|
+
this.arrow.lineTo(arrowXLocal, tip);
|
|
4248
|
+
this.arrow.fill(this.host.tokens.plaqueDark);
|
|
4249
|
+
}
|
|
4250
|
+
// The card carries the scale as ONE transform around its own (0,0) local origin — equivalent to
|
|
4251
|
+
// the DOM's transform-origin:top left — so `p.x`/`p.y` (screen units) remain its visual top-left
|
|
4252
|
+
// regardless of `s`, and every local size above (background rect, scroll viewport, arrow) scales
|
|
4253
|
+
// with it automatically.
|
|
4254
|
+
this.card.scale.set(s);
|
|
4255
|
+
this.card.position.set(p.x, p.y);
|
|
4256
|
+
this._cardX = p.x;
|
|
4257
|
+
this._cardY = p.y;
|
|
4258
|
+
this._cardW = screenW;
|
|
4259
|
+
this._arrowX = p.arrowX;
|
|
4260
|
+
}
|
|
4261
|
+
/** Arrow keys scroll a long list; everything else (Escape included) goes to the controller. */
|
|
3823
4262
|
onKey(e) {
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
case 'ArrowUp':
|
|
3832
|
-
this.scroll.scrollBy(-lineStep);
|
|
3833
|
-
return true;
|
|
3834
|
-
case 'PageDown':
|
|
3835
|
-
this.scroll.scrollBy(pageStep);
|
|
3836
|
-
return true;
|
|
3837
|
-
case 'PageUp':
|
|
3838
|
-
this.scroll.scrollBy(-pageStep);
|
|
3839
|
-
return true;
|
|
3840
|
-
case 'Space':
|
|
3841
|
-
if (e.shiftKey) {
|
|
3842
|
-
this.scroll.scrollBy(-pageStep);
|
|
3843
|
-
}
|
|
3844
|
-
else {
|
|
3845
|
-
this.scroll.scrollBy(pageStep);
|
|
3846
|
-
}
|
|
3847
|
-
return true;
|
|
3848
|
-
case 'Home':
|
|
3849
|
-
this.scroll.scrollBy(-999999);
|
|
3850
|
-
return true;
|
|
3851
|
-
case 'End':
|
|
3852
|
-
this.scroll.scrollBy(999999);
|
|
3853
|
-
return true;
|
|
3854
|
-
default:
|
|
3855
|
-
return false;
|
|
4263
|
+
if (e.code === 'ArrowDown') {
|
|
4264
|
+
this.scroll.scrollBy(40);
|
|
4265
|
+
return true;
|
|
4266
|
+
}
|
|
4267
|
+
if (e.code === 'ArrowUp') {
|
|
4268
|
+
this.scroll.scrollBy(-40);
|
|
4269
|
+
return true;
|
|
3856
4270
|
}
|
|
4271
|
+
return false;
|
|
3857
4272
|
}
|
|
3858
4273
|
fit() {
|
|
3859
|
-
|
|
4274
|
+
this.resize(this.host.screenW, this.host.screenH);
|
|
3860
4275
|
}
|
|
3861
4276
|
onRemove() {
|
|
3862
4277
|
this.scroll.destroy({ children: true });
|
|
@@ -3982,6 +4397,49 @@ class Slider extends pixi_js.Container {
|
|
|
3982
4397
|
return { w: this.w, h: 24 };
|
|
3983
4398
|
}
|
|
3984
4399
|
}
|
|
4400
|
+
/** Pill switch — the Pixi twin of the DOM `.ge-toggle`. 42×24, knob 20, accent when on. */
|
|
4401
|
+
class Toggle extends pixi_js.Container {
|
|
4402
|
+
track = new pixi_js.Graphics();
|
|
4403
|
+
knob = new pixi_js.Graphics();
|
|
4404
|
+
_value;
|
|
4405
|
+
onChange;
|
|
4406
|
+
accent;
|
|
4407
|
+
offFill;
|
|
4408
|
+
constructor(value, onChange, accent = '#8b5cf6', off = 'rgba(255,255,255,.22)') {
|
|
4409
|
+
super();
|
|
4410
|
+
this._value = value;
|
|
4411
|
+
this.onChange = onChange;
|
|
4412
|
+
this.accent = accent;
|
|
4413
|
+
this.offFill = off;
|
|
4414
|
+
this.addChild(this.track, this.knob);
|
|
4415
|
+
this.eventMode = 'static';
|
|
4416
|
+
this.cursor = 'pointer';
|
|
4417
|
+
this.hitArea = new pixi_js.Rectangle(0, 0, 42, 24);
|
|
4418
|
+
this.on('pointertap', () => this.onChange(!this._value));
|
|
4419
|
+
this.paint();
|
|
4420
|
+
}
|
|
4421
|
+
get value() {
|
|
4422
|
+
return this._value;
|
|
4423
|
+
}
|
|
4424
|
+
setValue(v) {
|
|
4425
|
+
this._value = v;
|
|
4426
|
+
this.paint();
|
|
4427
|
+
}
|
|
4428
|
+
paint() {
|
|
4429
|
+
this.track.clear();
|
|
4430
|
+
this.track.roundRect(0, 0, 42, 24, 12);
|
|
4431
|
+
this.track.fill(this._value ? this.accent : this.offFill);
|
|
4432
|
+
this.knob.clear();
|
|
4433
|
+
this.knob.circle(this._value ? 30 : 12, 12, 10);
|
|
4434
|
+
this.knob.fill('#ffffff');
|
|
4435
|
+
}
|
|
4436
|
+
measureSize() {
|
|
4437
|
+
return { w: 42, h: 24 };
|
|
4438
|
+
}
|
|
4439
|
+
setLayoutSize() {
|
|
4440
|
+
/* fixed size */
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
3985
4443
|
/** Picker chip — `.ge-chip`. Selected = accent bg + accent border. */
|
|
3986
4444
|
class Chip extends pixi_js.Container {
|
|
3987
4445
|
id;
|
|
@@ -4030,112 +4488,297 @@ class Chip extends pixi_js.Container {
|
|
|
4030
4488
|
}
|
|
4031
4489
|
}
|
|
4032
4490
|
|
|
4033
|
-
/**
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4491
|
+
/** The bar menu as a Pixi popover. Same rows, same order as the DOM — both come from resolveMenu.
|
|
4492
|
+
*
|
|
4493
|
+
* `getAnchor` (the burger — the arrow's POINTER), `getPlate` (the bar's plaque — drives placement)
|
|
4494
|
+
* and `getScale` (the bar's own fit-scale) are all FUNCTIONS, resolved lazily on every reposition —
|
|
4495
|
+
* never a captured `BottomBar` instance. `PixiRenderer.renderBar()` destroys and rebuilds the
|
|
4496
|
+
* BottomBar on every resize AND on ~20 other state changes, in the SAME resize handler that then
|
|
4497
|
+
* repositions this popover. A captured instance would already be destroyed by the time `resize()`
|
|
4498
|
+
* next runs, so `menuAnchor()`/`menuPlate()` on it would return `null` and the card would silently
|
|
4499
|
+
* recentre with its arrow hidden — the exact bug the DOM renderer shipped and fixed the same way
|
|
4500
|
+
* (see `html/Menu.ts`'s plate/pointer callbacks). Passing getters means every call reads whichever
|
|
4501
|
+
* bar is CURRENT. `getPlate`/`getScale` are optional so every pre-existing caller (which only ever
|
|
4502
|
+
* passed `getAnchor`) keeps behaving exactly as it did before the plate/pointer/scale split. */
|
|
4503
|
+
function openMenu(host, getAnchor, getPlate, getScale) {
|
|
4504
|
+
const updaters = {};
|
|
4505
|
+
const layer = new Popover(host, {
|
|
4506
|
+
tag: 'menu',
|
|
4507
|
+
plate: () => getPlate?.() ?? null,
|
|
4508
|
+
pointer: () => getAnchor?.() ?? null,
|
|
4509
|
+
scale: () => getScale?.() ?? 1,
|
|
4038
4510
|
onClose: () => host.closeLayer(),
|
|
4039
|
-
build: (
|
|
4511
|
+
build: (width) => {
|
|
4512
|
+
const col = new FlexBox({ direction: 'column', align: 'stretch', gap: 6 });
|
|
4513
|
+
for (const row of resolveMenu(host))
|
|
4514
|
+
col.add(buildRow(host, row, width, updaters));
|
|
4515
|
+
return col;
|
|
4516
|
+
},
|
|
4040
4517
|
});
|
|
4518
|
+
host.setMenuRefresh((id, v) => updaters[id]?.(v));
|
|
4519
|
+
return layer;
|
|
4520
|
+
}
|
|
4521
|
+
function label(text) {
|
|
4522
|
+
return makeText(text, { size: 13, weight: '600', color: '#ffffff' });
|
|
4041
4523
|
}
|
|
4042
|
-
function
|
|
4043
|
-
const
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
color: host.tokens.plaqueLabel,
|
|
4051
|
-
hover: host.tokens.accent,
|
|
4052
|
-
activeColor: '#ffffff',
|
|
4053
|
-
active: soundOn0,
|
|
4054
|
-
onTap: () => host.setSound?.(!(host.soundOn ?? true)),
|
|
4524
|
+
function rowBox(host, name, column = false) {
|
|
4525
|
+
const box = new FlexBox({
|
|
4526
|
+
direction: column ? 'column' : 'row',
|
|
4527
|
+
align: column ? 'stretch' : 'center',
|
|
4528
|
+
gap: column ? 8 : 10,
|
|
4529
|
+
padding: { top: 10, bottom: 10, left: 12, right: 12 },
|
|
4530
|
+
minHeight: column ? undefined : 44,
|
|
4531
|
+
background: { fill: host.tokens.plaqueGlass, radius: 14 },
|
|
4055
4532
|
});
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4533
|
+
box.label = name;
|
|
4534
|
+
return box;
|
|
4535
|
+
}
|
|
4536
|
+
function buildRow(host, row, width, updaters) {
|
|
4537
|
+
if (row.kind === 'separator') {
|
|
4538
|
+
const sep = new pixi_js.Container();
|
|
4539
|
+
sep.label = 'menu-sep';
|
|
4540
|
+
const line = new pixi_js.Graphics().rect(4, 6, width - 8, 1).fill(host.tokens.plaqueLine);
|
|
4541
|
+
line.alpha = 0.5;
|
|
4542
|
+
sep.addChild(line);
|
|
4543
|
+
return sep;
|
|
4544
|
+
}
|
|
4545
|
+
if (row.kind === 'button') {
|
|
4546
|
+
const box = rowBox(host, `menu-row-${row.id}`);
|
|
4547
|
+
if (row.icon)
|
|
4548
|
+
box.add(makeIcon(row.icon, 20, '#ffffff'));
|
|
4549
|
+
const text = label(row.label);
|
|
4550
|
+
box.add(text);
|
|
4551
|
+
box.add(new Spacer(), { grow: 1 });
|
|
4552
|
+
if (row.chevron)
|
|
4553
|
+
box.add(makeIcon('chevronRight', 16, host.tokens.muted));
|
|
4554
|
+
if (!row.disabled) {
|
|
4555
|
+
box.setInteractive(true);
|
|
4556
|
+
box.on('pointertap', () => {
|
|
4557
|
+
host.closeLayer();
|
|
4558
|
+
row.select();
|
|
4559
|
+
});
|
|
4560
|
+
attachHover(box, () => { box.setBgFill(host.tokens.plaqueGlassHover); text.style.fill = host.tokens.accent; }, () => { box.setBgFill(host.tokens.plaqueGlass); text.style.fill = '#ffffff'; });
|
|
4561
|
+
}
|
|
4562
|
+
else {
|
|
4563
|
+
box.alpha = 0.5;
|
|
4564
|
+
}
|
|
4565
|
+
return box;
|
|
4566
|
+
}
|
|
4567
|
+
if (row.kind === 'toggle') {
|
|
4568
|
+
const box = rowBox(host, `menu-row-${row.id}`);
|
|
4569
|
+
const glyph = row.icon(row.get());
|
|
4570
|
+
const iconNode = glyph ? makeIcon(glyph, 20, '#ffffff') : null;
|
|
4571
|
+
if (iconNode)
|
|
4572
|
+
box.add(iconNode);
|
|
4573
|
+
box.add(label(row.label));
|
|
4574
|
+
box.add(new Spacer(), { grow: 1 });
|
|
4575
|
+
// Disabled: neutralise the write-through at the source (a no-op onChange) rather than relying
|
|
4576
|
+
// solely on eventMode — Toggle wires its own pointertap listener unconditionally in its
|
|
4577
|
+
// constructor, so a stray direct emit must still be harmless, not just unreachable by real hits.
|
|
4578
|
+
const toggle = new Toggle(row.get(), row.disabled ? () => { } : (v) => row.set(v), host.tokens.accent, host.tokens.plaqueLine);
|
|
4579
|
+
box.add(toggle);
|
|
4580
|
+
updaters[row.id] = (v) => {
|
|
4581
|
+
const on = v === true;
|
|
4582
|
+
toggle.setValue(on);
|
|
4583
|
+
const next = row.icon(on);
|
|
4584
|
+
if (iconNode && next)
|
|
4585
|
+
iconNode.setIcon(next);
|
|
4586
|
+
};
|
|
4587
|
+
if (row.disabled) {
|
|
4588
|
+
box.alpha = 0.5;
|
|
4589
|
+
toggle.eventMode = 'none'; // dimmed + inert — mirrors the button branch's disabled treatment
|
|
4590
|
+
}
|
|
4591
|
+
return box;
|
|
4592
|
+
}
|
|
4593
|
+
const box = rowBox(host, `menu-row-${row.id}`, true);
|
|
4594
|
+
const head = new FlexBox({ direction: 'row', align: 'center' });
|
|
4595
|
+
const value = makeText(row.format(row.get()), { size: 12, weight: '700', color: host.tokens.plaqueLabel });
|
|
4596
|
+
head.add(label(row.label));
|
|
4597
|
+
head.add(new Spacer(), { grow: 1 });
|
|
4598
|
+
head.add(value);
|
|
4599
|
+
// Slider works in 0..1; map to the row's declared bounds, snapping to the MIN-anchored lattice
|
|
4600
|
+
// (min + k·step). Snapping to a bare multiple of step instead (Math.round(raw/step)*step) drifts
|
|
4601
|
+
// off `min` whenever min isn't itself a multiple of step — the common case, since the default step
|
|
4602
|
+
// is (max-min)/20: e.g. min:1,max:10 gives step 0.45, and the bare-multiple formula would emit 0.9
|
|
4603
|
+
// at the far left, BELOW the declared min. Clamp guards any float overshoot past either end.
|
|
4604
|
+
const toUnit = (v) => (row.max === row.min ? 0 : (v - row.min) / (row.max - row.min));
|
|
4605
|
+
const fromUnit = (u) => {
|
|
4606
|
+
const raw = row.min + u * (row.max - row.min);
|
|
4607
|
+
const snapped = row.min + Math.round((raw - row.min) / row.step) * row.step;
|
|
4608
|
+
return Math.max(row.min, Math.min(row.max, snapped));
|
|
4609
|
+
};
|
|
4610
|
+
// Disabled: same reasoning as the toggle branch above — a no-op onInput, not just eventMode.
|
|
4611
|
+
const slider = new Slider(host, toUnit(row.get()), row.disabled ? () => { } : (u) => {
|
|
4612
|
+
const v = fromUnit(u);
|
|
4613
|
+
value.text = row.format(v);
|
|
4614
|
+
head.layout(); // the readout's text just changed width (e.g. "5%" → "100%") — reposition it
|
|
4615
|
+
row.set(v);
|
|
4062
4616
|
});
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
col.add(infoRow);
|
|
4077
|
-
return col;
|
|
4617
|
+
updaters[row.id] = (v) => {
|
|
4618
|
+
const n = Number(v);
|
|
4619
|
+
value.text = row.format(n);
|
|
4620
|
+
head.layout();
|
|
4621
|
+
slider.setValue(toUnit(n));
|
|
4622
|
+
};
|
|
4623
|
+
box.add(head);
|
|
4624
|
+
box.add(slider);
|
|
4625
|
+
if (row.disabled) {
|
|
4626
|
+
box.alpha = 0.5;
|
|
4627
|
+
slider.eventMode = 'none';
|
|
4628
|
+
}
|
|
4629
|
+
return box;
|
|
4078
4630
|
}
|
|
4079
|
-
|
|
4080
|
-
|
|
4631
|
+
|
|
4632
|
+
function clamp(min, pref, max) {
|
|
4633
|
+
return Math.max(min, Math.min(max, pref));
|
|
4081
4634
|
}
|
|
4082
|
-
/**
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4635
|
+
/** Small rounded icon nav button — the overlay header back/close and row chevrons.
|
|
4636
|
+
* `.ge-ov-nav`: 32×32, radius 9, plaque-dark bg, white icon, hover → glass bg + accent icon. */
|
|
4637
|
+
function navButton(host, iconName, onTap, size = 32) {
|
|
4638
|
+
const root = new pixi_js.Container();
|
|
4639
|
+
const bg = new pixi_js.Graphics();
|
|
4640
|
+
const draw = (hover) => {
|
|
4641
|
+
bg.clear();
|
|
4642
|
+
bg.roundRect(0, 0, size, size, 9);
|
|
4643
|
+
bg.fill(hover ? host.tokens.plaqueGlass : host.tokens.plaqueDark);
|
|
4644
|
+
};
|
|
4645
|
+
draw(false);
|
|
4646
|
+
const gs = Math.round(size * 0.5625); // glyph tracks the box (default 32 → 18, unchanged)
|
|
4647
|
+
const glyph = makeIcon(iconName, gs, '#ffffff');
|
|
4648
|
+
glyph.position.set((size - gs) / 2, (size - gs) / 2);
|
|
4649
|
+
root.addChild(bg, glyph);
|
|
4650
|
+
root.eventMode = 'static';
|
|
4651
|
+
root.cursor = 'pointer';
|
|
4652
|
+
root.hitArea = new pixi_js.Rectangle(0, 0, size, size);
|
|
4653
|
+
attachHover(root, () => {
|
|
4654
|
+
draw(true);
|
|
4655
|
+
glyph.setColor(host.tokens.accent);
|
|
4656
|
+
}, () => {
|
|
4657
|
+
draw(false);
|
|
4658
|
+
glyph.setColor('#ffffff');
|
|
4093
4659
|
});
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4660
|
+
attachPress(root, 0.92, onTap);
|
|
4661
|
+
return root;
|
|
4662
|
+
}
|
|
4663
|
+
/** Full-screen overlay: frosted dark veil, fixed header (title + back/close), scrolling body.
|
|
4664
|
+
* The body is rebuilt on resize so it reflows like the CSS overlay. */
|
|
4665
|
+
class Overlay extends pixi_js.Container {
|
|
4666
|
+
tag;
|
|
4667
|
+
host;
|
|
4668
|
+
opts;
|
|
4669
|
+
veil = new pixi_js.Graphics();
|
|
4670
|
+
header = new pixi_js.Container();
|
|
4671
|
+
scroll;
|
|
4672
|
+
titleNode = new pixi_js.Container();
|
|
4673
|
+
nav = new pixi_js.Container();
|
|
4674
|
+
w = 0;
|
|
4675
|
+
h = 0;
|
|
4676
|
+
headerH = 44;
|
|
4677
|
+
/** Expose the scroll box for keyboard/test access. */
|
|
4678
|
+
get scrollContent() { return this.scroll; }
|
|
4679
|
+
constructor(host, opts) {
|
|
4680
|
+
super();
|
|
4681
|
+
this.host = host;
|
|
4682
|
+
this.opts = opts;
|
|
4683
|
+
this.tag = opts.tag;
|
|
4684
|
+
this.scroll = new ScrollBox(host.canvas);
|
|
4685
|
+
this.addChild(this.veil, this.scroll, this.header);
|
|
4686
|
+
this.veil.eventMode = 'static'; // swallow clicks to the game behind
|
|
4687
|
+
this.resize(host.screenW, host.screenH);
|
|
4688
|
+
}
|
|
4689
|
+
resize(w, h) {
|
|
4690
|
+
this.w = w;
|
|
4691
|
+
this.h = h;
|
|
4692
|
+
const vh = h / 100;
|
|
4693
|
+
this.headerH = clamp(40, 6.4 * vh, 52);
|
|
4694
|
+
// veil
|
|
4695
|
+
this.veil.clear();
|
|
4696
|
+
this.veil.rect(0, 0, w, h);
|
|
4697
|
+
this.veil.fill(this.host.tokens.backdrop);
|
|
4698
|
+
this.veil.hitArea = new pixi_js.Rectangle(0, 0, w, h);
|
|
4699
|
+
this.buildHeader();
|
|
4700
|
+
this.layoutBody();
|
|
4701
|
+
}
|
|
4702
|
+
buildHeader() {
|
|
4703
|
+
this.header.removeChildren().forEach((c) => c.destroy({ children: true }));
|
|
4704
|
+
const pad = 10;
|
|
4705
|
+
// back or spacer (left), centred title, close (right)
|
|
4706
|
+
const left = this.opts.onBack
|
|
4707
|
+
? navButton(this.host, 'back', () => this.opts.onBack())
|
|
4708
|
+
: new pixi_js.Container();
|
|
4709
|
+
const close = navButton(this.host, 'close', () => this.opts.onClose());
|
|
4710
|
+
const titleSize = clamp(13, 2.6 * (this.h / 100), 16);
|
|
4711
|
+
const title = makeText(this.opts.title, {
|
|
4712
|
+
size: titleSize,
|
|
4713
|
+
weight: '800',
|
|
4714
|
+
color: '#ffffff',
|
|
4715
|
+
letterSpacing: titleSize * 0.04,
|
|
4716
|
+
upper: true,
|
|
4717
|
+
align: 'center',
|
|
4109
4718
|
});
|
|
4719
|
+
title.anchor.set(0.5);
|
|
4720
|
+
title.position.set(this.w / 2, this.headerH / 2 + pad / 2);
|
|
4721
|
+
left.position.set(pad, (this.headerH - 32) / 2 + pad / 2);
|
|
4722
|
+
close.position.set(this.w - 32 - pad, (this.headerH - 32) / 2 + pad / 2);
|
|
4723
|
+
this.header.addChild(title, left, close);
|
|
4724
|
+
}
|
|
4725
|
+
layoutBody() {
|
|
4726
|
+
const top = this.headerH + 6;
|
|
4727
|
+
const sidePad = clamp(16, 4 * (this.w / 100), 24);
|
|
4728
|
+
const vPad = clamp(6, 2 * (this.h / 100), 16);
|
|
4729
|
+
const bodyW = Math.min(800, this.w - sidePad * 2);
|
|
4730
|
+
this.scroll.position.set(0, top);
|
|
4731
|
+
this.scroll.setViewport(this.w, this.h - top);
|
|
4732
|
+
this.scroll.content.removeChildren().forEach((c) => c.destroy({ children: true }));
|
|
4733
|
+
const content = this.opts.build(bodyW);
|
|
4734
|
+
if (content instanceof FlexBox)
|
|
4735
|
+
content.setLayoutSize(bodyW, undefined);
|
|
4736
|
+
content.position.set((this.w - bodyW) / 2, vPad);
|
|
4737
|
+
this.scroll.content.addChild(content);
|
|
4738
|
+
this.scroll.refresh();
|
|
4739
|
+
}
|
|
4740
|
+
/** Handle keyboard navigation while this overlay is the top layer. */
|
|
4741
|
+
onKey(e) {
|
|
4742
|
+
const viewH = this.h - (this.headerH + 6); // body viewport height
|
|
4743
|
+
const lineStep = 60;
|
|
4744
|
+
const pageStep = Math.floor(viewH * 0.9);
|
|
4745
|
+
switch (e.code) {
|
|
4746
|
+
case 'ArrowDown':
|
|
4747
|
+
this.scroll.scrollBy(lineStep);
|
|
4748
|
+
return true;
|
|
4749
|
+
case 'ArrowUp':
|
|
4750
|
+
this.scroll.scrollBy(-lineStep);
|
|
4751
|
+
return true;
|
|
4752
|
+
case 'PageDown':
|
|
4753
|
+
this.scroll.scrollBy(pageStep);
|
|
4754
|
+
return true;
|
|
4755
|
+
case 'PageUp':
|
|
4756
|
+
this.scroll.scrollBy(-pageStep);
|
|
4757
|
+
return true;
|
|
4758
|
+
case 'Space':
|
|
4759
|
+
if (e.shiftKey) {
|
|
4760
|
+
this.scroll.scrollBy(-pageStep);
|
|
4761
|
+
}
|
|
4762
|
+
else {
|
|
4763
|
+
this.scroll.scrollBy(pageStep);
|
|
4764
|
+
}
|
|
4765
|
+
return true;
|
|
4766
|
+
case 'Home':
|
|
4767
|
+
this.scroll.scrollBy(-999999);
|
|
4768
|
+
return true;
|
|
4769
|
+
case 'End':
|
|
4770
|
+
this.scroll.scrollBy(999999);
|
|
4771
|
+
return true;
|
|
4772
|
+
default:
|
|
4773
|
+
return false;
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
fit() {
|
|
4777
|
+
/* overlays scroll; no card fit needed */
|
|
4778
|
+
}
|
|
4779
|
+
onRemove() {
|
|
4780
|
+
this.scroll.destroy({ children: true });
|
|
4110
4781
|
}
|
|
4111
|
-
return row;
|
|
4112
|
-
}
|
|
4113
|
-
/** A column row: head (label + live % value) over a draggable slider. */
|
|
4114
|
-
function sliderRow(host, bodyWidth, key, label, updaters) {
|
|
4115
|
-
const row = new FlexBox({
|
|
4116
|
-
direction: 'column',
|
|
4117
|
-
align: 'stretch',
|
|
4118
|
-
gap: 10,
|
|
4119
|
-
padding: { top: 15, bottom: 15, left: 16, right: 16 },
|
|
4120
|
-
background: { fill: host.tokens.plaqueGlass, radius: 16 },
|
|
4121
|
-
});
|
|
4122
|
-
const head = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between' });
|
|
4123
|
-
const initial = host.getVolume(key);
|
|
4124
|
-
const valueText = makeText(`${Math.round(initial * 100)}%`, { size: 13, weight: '700', color: host.tokens.plaqueLabel });
|
|
4125
|
-
head.add(makeText(label, { size: 14, weight: '600', color: '#ffffff' }));
|
|
4126
|
-
head.add(new Spacer(), { grow: 1 });
|
|
4127
|
-
head.add(valueText);
|
|
4128
|
-
const slider = new Slider(host, initial, (v) => {
|
|
4129
|
-
valueText.text = `${Math.round(v * 100)}%`;
|
|
4130
|
-
host.setVolume(key, v);
|
|
4131
|
-
});
|
|
4132
|
-
updaters[key] = (v) => {
|
|
4133
|
-
valueText.text = `${Math.round(v * 100)}%`;
|
|
4134
|
-
slider.setValue(v);
|
|
4135
|
-
};
|
|
4136
|
-
row.add(head);
|
|
4137
|
-
row.add(slider);
|
|
4138
|
-
return row;
|
|
4139
4782
|
}
|
|
4140
4783
|
|
|
4141
4784
|
/** Default order key for the auto-injected hotkeys section: just after `controls` (-1). */
|
|
@@ -4148,7 +4791,7 @@ function openGameInfo(host) {
|
|
|
4148
4791
|
onClose: () => host.closeLayer(),
|
|
4149
4792
|
onBack: () => {
|
|
4150
4793
|
host.closeLayer();
|
|
4151
|
-
host.actions.
|
|
4794
|
+
host.actions.openMenu();
|
|
4152
4795
|
},
|
|
4153
4796
|
build: (w) => buildBody(host, w),
|
|
4154
4797
|
});
|
|
@@ -5955,6 +6598,15 @@ class PixiRenderer {
|
|
|
5955
6598
|
this.bar = new BottomBar(this.ctx);
|
|
5956
6599
|
this.barLayer.addChild(this.bar);
|
|
5957
6600
|
this.bar.applyFit();
|
|
6601
|
+
// renderBar() runs on every resize AND on ~20 other state changes (bet/win/turbo/mode/…), any of
|
|
6602
|
+
// which can change the bar's own fitScale()/menuPlate() (e.g. a WIN pill appearing mid-autoplay
|
|
6603
|
+
// retriggers the wide layout's overflow-tightening branch). Only onResize used to reposition the
|
|
6604
|
+
// open layer, so a live bar-content change while the menu was open left it at the stale
|
|
6605
|
+
// scale/position until the next resize. Every ShellLayer's resize() only re-reads current
|
|
6606
|
+
// geometry and re-fits/re-centres itself — none of them call back into renderBar() (verified:
|
|
6607
|
+
// Popover, CardModal, Overlay, BuyBonusOverlay) — so this cannot recurse; `currentLayer` is
|
|
6608
|
+
// `null` whenever nothing is open, so this cannot throw either.
|
|
6609
|
+
this.currentLayer?.resize?.(this.screenW, this.screenH);
|
|
5958
6610
|
}
|
|
5959
6611
|
setLayout() {
|
|
5960
6612
|
this.renderBar();
|
|
@@ -5988,8 +6640,13 @@ class PixiRenderer {
|
|
|
5988
6640
|
openOverlay(req) {
|
|
5989
6641
|
let layer = null;
|
|
5990
6642
|
switch (req.kind) {
|
|
5991
|
-
case '
|
|
5992
|
-
|
|
6643
|
+
case 'menu':
|
|
6644
|
+
// Getters, not `this.bar` by value: renderBar() destroys/rebuilds the bar on every resize
|
|
6645
|
+
// and ~20 other state changes, in the same resize handler that then repositions this popover
|
|
6646
|
+
// — see Menu.ts's openMenu doc comment for why this must stay lazy. menuAnchor (the burger)
|
|
6647
|
+
// is the arrow's pointer; menuPlate (the plaque) drives placement; fitScale is the same
|
|
6648
|
+
// factor BottomBar applies to its own content via `inner.scale`.
|
|
6649
|
+
layer = openMenu(this.ctx, () => this.bar?.menuAnchor() ?? null, () => this.bar?.menuPlate() ?? null, () => this.bar?.fitScale() ?? 1);
|
|
5993
6650
|
break;
|
|
5994
6651
|
case 'gameInfo':
|
|
5995
6652
|
layer = openGameInfo(this.ctx);
|
|
@@ -6012,7 +6669,7 @@ class PixiRenderer {
|
|
|
6012
6669
|
}
|
|
6013
6670
|
if (!layer)
|
|
6014
6671
|
return;
|
|
6015
|
-
this.pushLayer(layer);
|
|
6672
|
+
this.pushLayer(layer, { backdrop: req.kind !== 'menu' });
|
|
6016
6673
|
const built = layer;
|
|
6017
6674
|
return {
|
|
6018
6675
|
onKey: built.onKey ? built.onKey.bind(built) : undefined,
|
|
@@ -6022,10 +6679,6 @@ class PixiRenderer {
|
|
|
6022
6679
|
closeOverlay() {
|
|
6023
6680
|
this.closeLayer();
|
|
6024
6681
|
}
|
|
6025
|
-
refreshSoundIcon(_on) {
|
|
6026
|
-
// No-op: the open Settings overlay registers its icon updater via host.setSoundRefresh, which
|
|
6027
|
-
// the controller's setSound path drives. The renderer has nothing extra to refresh.
|
|
6028
|
-
}
|
|
6029
6682
|
/** Fade out (≈250ms, like GameShell's REMOVE_FADE_MS) then tear down; resolves when removed. */
|
|
6030
6683
|
destroy() {
|
|
6031
6684
|
if (this.destroyed)
|
|
@@ -6049,9 +6702,12 @@ class PixiRenderer {
|
|
|
6049
6702
|
});
|
|
6050
6703
|
}
|
|
6051
6704
|
// ── layer stack ────────────────────────────────────────────────────────────
|
|
6052
|
-
pushLayer(node) {
|
|
6705
|
+
pushLayer(node, opts) {
|
|
6053
6706
|
this.clearLayer();
|
|
6054
|
-
|
|
6707
|
+
// Light-dismiss layers (the menu popover) opt out with `{ backdrop: false }` — no frosted
|
|
6708
|
+
// snapshot, the game stays visible behind them. Every other caller is unaffected (defaults on).
|
|
6709
|
+
if (opts?.backdrop !== false)
|
|
6710
|
+
this.makeBackdrop(); // frosted snapshot (the DOM's backdrop-filter:blur)
|
|
6055
6711
|
this.currentLayer = node;
|
|
6056
6712
|
this.modalLayer.addChild(node);
|
|
6057
6713
|
this.fitModals();
|
|
@@ -6173,6 +6829,7 @@ class PixiRenderer {
|
|
|
6173
6829
|
get tokens() { return host.tokens; },
|
|
6174
6830
|
get layout() { return host.layout; },
|
|
6175
6831
|
get soundOn() { return host.soundOn; },
|
|
6832
|
+
get menu() { return host.menu; },
|
|
6176
6833
|
get actions() { return host.actions; },
|
|
6177
6834
|
openReplay: (opts) => host.openReplay(opts),
|
|
6178
6835
|
t: (s) => host.t(s),
|
|
@@ -6180,17 +6837,18 @@ class PixiRenderer {
|
|
|
6180
6837
|
emit: host.emit.bind(host),
|
|
6181
6838
|
notifyResize: (w, h) => host.notifyResize(w, h),
|
|
6182
6839
|
setSound: (on) => host.setSound(on),
|
|
6183
|
-
setSoundRefresh: (fn) => host.setSoundRefresh(fn),
|
|
6184
6840
|
getVolume: (key) => host.getVolume(key),
|
|
6185
6841
|
setVolume: (key, v) => host.setVolume(key, v),
|
|
6186
|
-
|
|
6842
|
+
getMenuValue: (id) => host.getMenuValue(id),
|
|
6843
|
+
setMenuValue: (id, v) => host.setMenuValue(id, v),
|
|
6844
|
+
setMenuRefresh: (fn) => host.setMenuRefresh(fn),
|
|
6187
6845
|
// — Pixi-specific surface —
|
|
6188
6846
|
get ticker() { return self.app.ticker; },
|
|
6189
6847
|
get canvas() { return self.app.canvas; },
|
|
6190
6848
|
get screenW() { return self.app.screen.width; },
|
|
6191
6849
|
get screenH() { return self.app.screen.height; },
|
|
6192
6850
|
render: () => self.renderBar(),
|
|
6193
|
-
pushLayer: (node) => self.pushLayer(node),
|
|
6851
|
+
pushLayer: (node, opts) => self.pushLayer(node, opts),
|
|
6194
6852
|
// Route component-initiated closes through the controller (not straight to self.closeLayer) so
|
|
6195
6853
|
// it clears its OverlayHandle. Otherwise the handle goes stale: hasOpenLayer() stays true and
|
|
6196
6854
|
// keydowns keep routing to onKey on a destroyed overlay → write to a torn-down ScrollBox.
|
|
@@ -6225,16 +6883,24 @@ function removePixiShell() {
|
|
|
6225
6883
|
}
|
|
6226
6884
|
|
|
6227
6885
|
exports.DEFAULT_ACCENT = DEFAULT_ACCENT;
|
|
6886
|
+
exports.DEFAULT_MENU = DEFAULT_MENU;
|
|
6228
6887
|
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
|
6888
|
+
exports.POPOVER = POPOVER;
|
|
6229
6889
|
exports.PixiRenderer = PixiRenderer;
|
|
6230
6890
|
exports.SCHEMES = SCHEMES;
|
|
6231
6891
|
exports.ShellController = ShellController;
|
|
6232
6892
|
exports.createI18n = createI18n;
|
|
6233
6893
|
exports.createPixiShell = createPixiShell;
|
|
6234
6894
|
exports.createShell = createShell;
|
|
6895
|
+
exports.isPresetId = isPresetId;
|
|
6235
6896
|
exports.normalizeLang = normalizeLang;
|
|
6897
|
+
exports.placePopover = placePopover;
|
|
6898
|
+
exports.popoverWidth = popoverWidth;
|
|
6899
|
+
exports.rangeBounds = rangeBounds;
|
|
6236
6900
|
exports.removePixiShell = removePixiShell;
|
|
6237
6901
|
exports.resolveConfig = resolveConfig;
|
|
6902
|
+
exports.resolveMenu = resolveMenu;
|
|
6238
6903
|
exports.resolveTheme = resolveTheme;
|
|
6904
|
+
exports.seedMenuValues = seedMenuValues;
|
|
6239
6905
|
exports.socialize = socialize;
|
|
6240
6906
|
//# sourceMappingURL=pixi.cjs.js.map
|