@energy8platform/shell 0.2.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 +3894 -0
- package/dist/html.cjs.js.map +1 -0
- package/dist/html.d.ts +616 -0
- package/dist/html.esm.js +3879 -0
- package/dist/html.esm.js.map +1 -0
- package/dist/index.cjs.js +1593 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +554 -0
- package/dist/index.esm.js +1582 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/pixi.cjs.js +5740 -0
- package/dist/pixi.cjs.js.map +1 -0
- package/dist/pixi.d.ts +682 -0
- package/dist/pixi.esm.js +5726 -0
- package/dist/pixi.esm.js.map +1 -0
- package/package.json +79 -0
- package/src/core/EventEmitter.ts +55 -0
- package/src/core/ShellController.ts +234 -0
- package/src/core/colors.ts +32 -0
- package/src/core/fonts-digits.ts +12 -0
- package/src/core/fonts.ts +13 -0
- package/src/core/format.ts +39 -0
- package/src/core/i18n.ts +96 -0
- package/src/core/index.ts +35 -0
- package/src/core/keyboard.ts +229 -0
- package/src/core/locales.ts +864 -0
- package/src/core/motion.ts +10 -0
- package/src/core/renderer.ts +121 -0
- package/src/core/state.ts +31 -0
- package/src/core/theme.ts +81 -0
- package/src/core/types.ts +269 -0
- package/src/core/version.ts +3 -0
- package/src/ui/html/HtmlRenderer.ts +292 -0
- package/src/ui/html/components/BottomBar.ts +240 -0
- package/src/ui/html/components/BuyBonus.ts +326 -0
- package/src/ui/html/components/GameInfo.ts +384 -0
- package/src/ui/html/components/Modal.ts +36 -0
- package/src/ui/html/components/ReplayModal.ts +58 -0
- package/src/ui/html/components/Settings.ts +59 -0
- package/src/ui/html/components/pickers.ts +146 -0
- package/src/ui/html/icons-preview.svg +224 -0
- package/src/ui/html/icons.ts +31 -0
- package/src/ui/html/index.ts +36 -0
- package/src/ui/html/motion-dom.ts +29 -0
- package/src/ui/html/primitives.ts +85 -0
- package/src/ui/html/shell.css.ts +528 -0
- package/src/ui/html/theme-css.ts +21 -0
- package/src/ui/pixi/PixiRenderer.ts +350 -0
- package/src/ui/pixi/components/BottomBar.ts +477 -0
- package/src/ui/pixi/components/BuyBonus.ts +763 -0
- package/src/ui/pixi/components/GameInfo.ts +559 -0
- package/src/ui/pixi/components/Modal.ts +40 -0
- package/src/ui/pixi/components/ReplayModal.ts +80 -0
- package/src/ui/pixi/components/Settings.ts +117 -0
- package/src/ui/pixi/components/pickers.ts +170 -0
- package/src/ui/pixi/context.ts +52 -0
- package/src/ui/pixi/icons.ts +45 -0
- package/src/ui/pixi/index.ts +56 -0
- package/src/ui/pixi/motion-pixi.ts +58 -0
- package/src/ui/pixi/pixi-icon.ts +119 -0
- package/src/ui/pixi/primitives/card.ts +227 -0
- package/src/ui/pixi/primitives/controls.ts +243 -0
- package/src/ui/pixi/primitives/flex.ts +335 -0
- package/src/ui/pixi/primitives/overlay.ts +181 -0
- package/src/ui/pixi/primitives/scroll.ts +117 -0
- package/src/ui/pixi/primitives/widgets.ts +680 -0
- package/src/ui/pixi/text.ts +131 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type { ShellHost } from '@/core/renderer';
|
|
2
|
+
import type { BonusOption } from '@/core/types';
|
|
3
|
+
import { betDir } from '@/core/keyboard';
|
|
4
|
+
import { effectiveAccent, contrastText } from '@/core/colors';
|
|
5
|
+
import { createOverlay, createCardModal } from '../primitives';
|
|
6
|
+
import { icon, type IconName } from '../icons';
|
|
7
|
+
|
|
8
|
+
/** Mutable state shared between the overlay DOM and the onKey handler. */
|
|
9
|
+
interface OverlayState {
|
|
10
|
+
/** Index into the affordable-card subset; -1 = none (no affordable cards). */
|
|
11
|
+
focusIndex: number;
|
|
12
|
+
/** The bonus whose confirm dialog is currently open, or undefined. */
|
|
13
|
+
confirmBonus: BonusOption | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Buy-bonus overlay — a grid of art-forward cards, one per option.
|
|
17
|
+
* Returns the overlay element + a keyboard handler for the shell's `showModal`. */
|
|
18
|
+
export function openBuyBonusOverlay(host: ShellHost): { root: HTMLElement; onKey: (e: KeyboardEvent) => boolean } | null {
|
|
19
|
+
const bonuses = host.config.features.buyBonus;
|
|
20
|
+
if (bonuses === false || bonuses.length === 0) return null;
|
|
21
|
+
|
|
22
|
+
const st: OverlayState = { focusIndex: -1, confirmBonus: undefined };
|
|
23
|
+
|
|
24
|
+
const { root, body } = createOverlay({ title: host.t('Buy bonus'), onClose: () => host.actions.closeOverlay() });
|
|
25
|
+
root.dataset.ge = 'buybonus-overlay';
|
|
26
|
+
|
|
27
|
+
// Re-render the grid whenever the bet changes so every card's price stays live.
|
|
28
|
+
const renderGrid = (): void => {
|
|
29
|
+
body.innerHTML = '';
|
|
30
|
+
const grid = document.createElement('div'); grid.className = 'ge-bb-grid';
|
|
31
|
+
// Card count drives the width-fit clamp in CSS (each card is 18em; N cards must fit the frame
|
|
32
|
+
// width), so the row scales to the available width instead of overflowing into an X-scroll.
|
|
33
|
+
grid.style.setProperty('--ge-bb-n', String(bonuses.length));
|
|
34
|
+
const affordable: BonusOption[] = [];
|
|
35
|
+
for (const bonus of bonuses) {
|
|
36
|
+
const card = buildCard(host, bonus, root, st);
|
|
37
|
+
grid.appendChild(card);
|
|
38
|
+
if (isAffordable(host, bonus)) affordable.push(bonus);
|
|
39
|
+
}
|
|
40
|
+
body.appendChild(grid);
|
|
41
|
+
// Initialize or restore focus index
|
|
42
|
+
if (affordable.length > 0) {
|
|
43
|
+
if (st.focusIndex < 0) st.focusIndex = 0;
|
|
44
|
+
else st.focusIndex = Math.min(st.focusIndex, affordable.length - 1);
|
|
45
|
+
applyFocusClass(root, bonuses, affordable, st.focusIndex);
|
|
46
|
+
} else {
|
|
47
|
+
st.focusIndex = -1;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
renderGrid();
|
|
52
|
+
root.appendChild(buildBetBar(host, renderGrid)); // thin bottom footer, only as tall as the pill
|
|
53
|
+
|
|
54
|
+
/** Step the bet by `dir` and re-render the grid (live prices + affordability) when it changed.
|
|
55
|
+
* Shared by the keyboard bet keys (the footer ± buttons keep their own copy). */
|
|
56
|
+
const stepBetBy = (dir: 1 | -1): void => {
|
|
57
|
+
const prev = host.state.bet;
|
|
58
|
+
host.actions.stepBet(dir);
|
|
59
|
+
// host.state.bet is updated synchronously by the controller action.
|
|
60
|
+
if (host.state.bet !== prev) renderGrid();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Keyboard handler for both browse and confirm phases. */
|
|
64
|
+
const onKey = (e: KeyboardEvent): boolean => {
|
|
65
|
+
const affordable = bonuses.filter((b) => isAffordable(host, b));
|
|
66
|
+
|
|
67
|
+
// ── Confirm phase ──
|
|
68
|
+
if (st.confirmBonus) {
|
|
69
|
+
switch (e.code) {
|
|
70
|
+
case 'Enter':
|
|
71
|
+
case 'Space': {
|
|
72
|
+
const bonus = st.confirmBonus;
|
|
73
|
+
if (!isAffordable(host, bonus)) return true;
|
|
74
|
+
if (bonus.type === 'feature') host.actions.activateFeature(bonus);
|
|
75
|
+
else host.actions.selectBuyBonus(bonus.id);
|
|
76
|
+
host.actions.closeOverlay();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
case 'Escape':
|
|
80
|
+
// Remove the confirm dialog, return to browse
|
|
81
|
+
closeConfirm(root, st);
|
|
82
|
+
return true;
|
|
83
|
+
default:
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Browse phase ──
|
|
89
|
+
const last = affordable.length - 1;
|
|
90
|
+
const mobile = host.layout === 'mobile';
|
|
91
|
+
|
|
92
|
+
// Bet stepping mirrors the bar's keys (Shift+↑/↓, Shift+=/-, Numpad ±). Checked BEFORE arrow
|
|
93
|
+
// navigation so a bare arrow still moves card focus while a Shift+arrow changes the bet.
|
|
94
|
+
const bet = betDir(e);
|
|
95
|
+
if (bet !== null) { stepBetBy(bet); return true; }
|
|
96
|
+
|
|
97
|
+
// Determine navigation direction from key code + layout (mobile uses vertical arrows)
|
|
98
|
+
const fwdKey = e.code === 'ArrowRight' || (mobile && e.code === 'ArrowDown');
|
|
99
|
+
const bwdKey = e.code === 'ArrowLeft' || (mobile && e.code === 'ArrowUp');
|
|
100
|
+
|
|
101
|
+
if (fwdKey) {
|
|
102
|
+
if (last < 0) return true;
|
|
103
|
+
if (st.focusIndex < last) {
|
|
104
|
+
st.focusIndex++;
|
|
105
|
+
applyFocusClass(root, bonuses, affordable, st.focusIndex);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (bwdKey) {
|
|
110
|
+
if (last < 0) return true;
|
|
111
|
+
if (st.focusIndex > 0) {
|
|
112
|
+
st.focusIndex--;
|
|
113
|
+
applyFocusClass(root, bonuses, affordable, st.focusIndex);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
switch (e.code) {
|
|
119
|
+
case 'Enter':
|
|
120
|
+
case 'Space':
|
|
121
|
+
if (last < 0 || st.focusIndex < 0) return true;
|
|
122
|
+
{
|
|
123
|
+
const bonus = affordable[st.focusIndex];
|
|
124
|
+
openConfirm(host, bonus, root, st);
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
// Bare =/- also step the bet (the Shift+=/- and Numpad variants are handled by betDir above).
|
|
128
|
+
case 'Equal':
|
|
129
|
+
stepBetBy(1);
|
|
130
|
+
return true;
|
|
131
|
+
case 'Minus':
|
|
132
|
+
stepBetBy(-1);
|
|
133
|
+
return true;
|
|
134
|
+
case 'Escape':
|
|
135
|
+
host.actions.closeOverlay();
|
|
136
|
+
return true;
|
|
137
|
+
default:
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return { root, onKey };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Apply a CSS keyboard-focus class to the currently focused affordable card. */
|
|
146
|
+
function applyFocusClass(overlay: HTMLElement, bonuses: BonusOption[], affordable: BonusOption[], focusIndex: number): void {
|
|
147
|
+
for (const b of bonuses) {
|
|
148
|
+
const card = overlay.querySelector(`[data-ge="bonus-card-${b.id}"]`) as HTMLElement | null;
|
|
149
|
+
if (!card) continue;
|
|
150
|
+
card.classList.remove('ge-bonus-card--kbd-focus');
|
|
151
|
+
}
|
|
152
|
+
const focused = affordable[focusIndex];
|
|
153
|
+
if (!focused) return;
|
|
154
|
+
const card = overlay.querySelector(`[data-ge="bonus-card-${focused.id}"]`) as HTMLElement | null;
|
|
155
|
+
if (card) card.classList.add('ge-bonus-card--kbd-focus');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Open the confirm dialog for the given bonus and track it in overlay state. */
|
|
159
|
+
function openConfirm(host: ShellHost, bonus: BonusOption, overlay: HTMLElement, st: OverlayState): void {
|
|
160
|
+
closeConfirm(overlay, st); // remove any existing confirm
|
|
161
|
+
st.confirmBonus = bonus;
|
|
162
|
+
overlay.appendChild(buildConfirm(host, bonus, overlay, st));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Remove the confirm dialog and clear the overlay state. */
|
|
166
|
+
function closeConfirm(overlay: HTMLElement, st: OverlayState): void {
|
|
167
|
+
// The confirm dialog is a .ge-sheet with data-ge="bonus-confirm" appended directly to overlay.
|
|
168
|
+
const sheet = overlay.querySelector('[data-ge="bonus-confirm"]') as HTMLElement | null;
|
|
169
|
+
if (sheet) sheet.remove();
|
|
170
|
+
st.confirmBonus = undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Bet control — a compact −/+ pill around the live stake, in a thin footer at the screen bottom.
|
|
174
|
+
* Stepping repaints the value, re-prices the cards, and updates the control bar. */
|
|
175
|
+
function buildBetBar(host: ShellHost, onChange: () => void): HTMLElement {
|
|
176
|
+
const bar = document.createElement('div'); bar.className = 'ge-bb-betbar';
|
|
177
|
+
const pill = document.createElement('div'); pill.className = 'ge-bb-betpill';
|
|
178
|
+
const val = document.createElement('div'); val.className = 'ge-bb-betval';
|
|
179
|
+
const down = stepButton('bb-bet-down', 'minus');
|
|
180
|
+
const up = stepButton('bb-bet-up', 'plus');
|
|
181
|
+
// Mirror the control bar: disable a stepper at the end of the bet range, and lock both
|
|
182
|
+
// while busy — so changing the stake behaves identically here and on the bottom bar.
|
|
183
|
+
const paint = () => {
|
|
184
|
+
val.innerHTML = `<span>${host.t('Bet')}</span><b>${host.formatCurrency(host.state.bet)}</b>`;
|
|
185
|
+
const i = host.state.availableBets.indexOf(host.state.bet);
|
|
186
|
+
down.disabled = host.state.busy || i <= 0;
|
|
187
|
+
up.disabled = host.state.busy || i >= host.state.availableBets.length - 1;
|
|
188
|
+
};
|
|
189
|
+
const step = (dir: 1 | -1) => () => {
|
|
190
|
+
const prev = host.state.bet;
|
|
191
|
+
host.actions.stepBet(dir);
|
|
192
|
+
if (host.state.bet !== prev) { paint(); onChange(); }
|
|
193
|
+
};
|
|
194
|
+
down.addEventListener('click', step(-1));
|
|
195
|
+
up.addEventListener('click', step(1));
|
|
196
|
+
paint();
|
|
197
|
+
pill.append(down, val, up);
|
|
198
|
+
bar.appendChild(pill);
|
|
199
|
+
return bar;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stepButton(ge: string, name: IconName): HTMLButtonElement {
|
|
203
|
+
const b = document.createElement('button');
|
|
204
|
+
b.className = 'ge-bb-betstep'; b.dataset.ge = ge; b.innerHTML = icon(name);
|
|
205
|
+
return b;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A grid card: title → thumbnail → description → volatility → price → full-bleed CTA.
|
|
209
|
+
* Clicking (when affordable) opens the confirmation modal. */
|
|
210
|
+
function buildCard(host: ShellHost, bonus: BonusOption, overlay: HTMLElement, st: OverlayState): HTMLElement {
|
|
211
|
+
const accent = effectiveAccent(bonus);
|
|
212
|
+
const card = document.createElement('div');
|
|
213
|
+
card.className = 'ge-bonus-card'; card.dataset.ge = `bonus-card-${bonus.id}`;
|
|
214
|
+
card.style.setProperty('--card-acc', accent);
|
|
215
|
+
card.style.setProperty('--card-ink', contrastText(accent));
|
|
216
|
+
|
|
217
|
+
const enabled = isAffordable(host, bonus);
|
|
218
|
+
// Stack the confirm on top of the overlay grid (cancel returns to the grid). Re-checks
|
|
219
|
+
// affordability at click time, so it's a safe no-op when the option can't be bought.
|
|
220
|
+
const select = (): void => {
|
|
221
|
+
if (!isAffordable(host, bonus)) return;
|
|
222
|
+
openConfirm(host, bonus, overlay, st);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Game-supplied card UI: the shell keeps the wrapper (grid sizing + accent vars) and runs the
|
|
226
|
+
// buy flow when the game calls ctx.select(); the game owns everything inside.
|
|
227
|
+
if (bonus.custom) {
|
|
228
|
+
card.classList.add('ge-bonus-card--custom');
|
|
229
|
+
const price = bonus.priceMultiplier * host.state.bet;
|
|
230
|
+
card.appendChild(bonus.custom({
|
|
231
|
+
bonus, bet: host.state.bet, price,
|
|
232
|
+
priceText: host.formatCurrency(price),
|
|
233
|
+
disabled: !enabled, accent, select,
|
|
234
|
+
}) as Node);
|
|
235
|
+
return card;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
card.appendChild(cardBody(host, bonus));
|
|
239
|
+
const cta = document.createElement('button');
|
|
240
|
+
cta.className = 'ge-bonus-cta'; cta.dataset.ge = `bonus-cta-${bonus.id}`;
|
|
241
|
+
cta.textContent = host.t(actionLabel(bonus));
|
|
242
|
+
card.appendChild(cta);
|
|
243
|
+
|
|
244
|
+
if (!enabled) {
|
|
245
|
+
card.classList.add('ge-bonus-off');
|
|
246
|
+
cta.disabled = true;
|
|
247
|
+
} else {
|
|
248
|
+
card.addEventListener('click', select);
|
|
249
|
+
}
|
|
250
|
+
return card;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The shared card interior (everything above the action area), reused by the confirm modal. */
|
|
254
|
+
function cardBody(host: ShellHost, bonus: BonusOption): HTMLElement {
|
|
255
|
+
const price = bonus.priceMultiplier * host.state.bet;
|
|
256
|
+
const wrap = document.createElement('div'); wrap.className = 'ge-bonus-body';
|
|
257
|
+
wrap.innerHTML =
|
|
258
|
+
`<div class="ge-bonus-title">${bonus.title}</div>` +
|
|
259
|
+
`<div class="ge-bonus-thumb">${thumb(bonus)}</div>` +
|
|
260
|
+
`<div class="ge-bonus-desc">${bonus.description}</div>` +
|
|
261
|
+
`<div class="ge-bonus-spacer"></div>` +
|
|
262
|
+
(bonus.volatility ? `<div class="ge-bonus-vol">${volatility(bonus.volatility)}</div>` : '') +
|
|
263
|
+
`<div class="ge-bonus-price">${host.formatCurrency(price)}</div>`;
|
|
264
|
+
return wrap;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Confirmation modal — the shared card chrome (accent title heading, no ✕) with a bonus
|
|
268
|
+
* preview body and a full-bleed Cancel + action footer. */
|
|
269
|
+
function buildConfirm(host: ShellHost, bonus: BonusOption, overlay: HTMLElement, st: OverlayState): HTMLElement {
|
|
270
|
+
const accent = effectiveAccent(bonus);
|
|
271
|
+
const ui = createCardModal({ ge: 'bonus-confirm', title: bonus.title, accent, onClose: () => { closeConfirm(overlay, st); } });
|
|
272
|
+
|
|
273
|
+
const price = bonus.priceMultiplier * host.state.bet;
|
|
274
|
+
const preview = document.createElement('div'); preview.className = 'ge-confirm-preview';
|
|
275
|
+
preview.innerHTML =
|
|
276
|
+
`<div class="ge-bonus-thumb">${thumb(bonus)}</div>` +
|
|
277
|
+
`<div class="ge-bonus-desc">${bonus.description}</div>` +
|
|
278
|
+
(bonus.volatility ? `<div class="ge-bonus-vol">${volatility(bonus.volatility)}</div>` : '') +
|
|
279
|
+
`<div class="ge-bonus-price">${host.formatCurrency(price)}</div>`;
|
|
280
|
+
ui.body.appendChild(preview);
|
|
281
|
+
|
|
282
|
+
const actions = document.createElement('div'); actions.className = 'ge-modal-actions';
|
|
283
|
+
const cancel = document.createElement('button');
|
|
284
|
+
cancel.className = 'ge-modal-btn ge-modal-btn--ghost'; cancel.dataset.ge = 'bonus-confirm-cancel';
|
|
285
|
+
cancel.textContent = host.t('Cancel');
|
|
286
|
+
cancel.addEventListener('click', () => closeConfirm(overlay, st));
|
|
287
|
+
const buy = document.createElement('button');
|
|
288
|
+
buy.className = 'ge-modal-btn ge-modal-btn--accent'; buy.dataset.ge = 'bonus-confirm-buy';
|
|
289
|
+
buy.textContent = host.t(actionLabel(bonus));
|
|
290
|
+
buy.style.color = contrastText(accent); // bg comes from --card-acc on the card
|
|
291
|
+
buy.addEventListener('click', () => {
|
|
292
|
+
// Re-check at click time: the confirm modal stays open across state changes, so a spin
|
|
293
|
+
// starting (busy), buy-bonus being disabled, or the balance dropping must block the purchase.
|
|
294
|
+
if (!isAffordable(host, bonus)) return;
|
|
295
|
+
if (bonus.type === 'feature') host.actions.activateFeature(bonus);
|
|
296
|
+
else host.actions.selectBuyBonus(bonus.id);
|
|
297
|
+
host.actions.closeOverlay();
|
|
298
|
+
});
|
|
299
|
+
actions.append(cancel, buy);
|
|
300
|
+
ui.card.appendChild(actions);
|
|
301
|
+
|
|
302
|
+
return ui.root;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function thumb(bonus: BonusOption): string {
|
|
306
|
+
if (bonus.thumbnail) return `<img src="${bonus.thumbnail}" alt="${bonus.title}">`;
|
|
307
|
+
return `<span class="ge-bonus-thumb-ph">${icon('gift')}</span>`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Volatility as five turbo bolts; the first `level` lit (white fill + accent ring), the rest
|
|
311
|
+
* inactive (white fill + black ring, dimmed) — same outline treatment as the turbo button. */
|
|
312
|
+
function volatility(level: number): string {
|
|
313
|
+
const n = Math.max(0, Math.min(5, level));
|
|
314
|
+
const bolt = icon('turbo1');
|
|
315
|
+
return `<span class="ge-bonus-vol-on">${bolt.repeat(n)}</span>` +
|
|
316
|
+
`<span class="ge-bonus-vol-off">${bolt.repeat(5 - n)}</span>`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function actionLabel(bonus: BonusOption): string {
|
|
320
|
+
return bonus.type === 'feature' ? 'Activate' : 'Buy';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isAffordable(host: ShellHost, bonus: BonusOption): boolean {
|
|
324
|
+
if (host.state.busy || !host.state.buyBonusEnabled) return false;
|
|
325
|
+
return bonus.priceMultiplier * host.state.bet <= host.state.balance;
|
|
326
|
+
}
|