@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.
Files changed (67) hide show
  1. package/dist/html.cjs.js +3894 -0
  2. package/dist/html.cjs.js.map +1 -0
  3. package/dist/html.d.ts +616 -0
  4. package/dist/html.esm.js +3879 -0
  5. package/dist/html.esm.js.map +1 -0
  6. package/dist/index.cjs.js +1593 -0
  7. package/dist/index.cjs.js.map +1 -0
  8. package/dist/index.d.ts +554 -0
  9. package/dist/index.esm.js +1582 -0
  10. package/dist/index.esm.js.map +1 -0
  11. package/dist/pixi.cjs.js +5740 -0
  12. package/dist/pixi.cjs.js.map +1 -0
  13. package/dist/pixi.d.ts +682 -0
  14. package/dist/pixi.esm.js +5726 -0
  15. package/dist/pixi.esm.js.map +1 -0
  16. package/package.json +79 -0
  17. package/src/core/EventEmitter.ts +55 -0
  18. package/src/core/ShellController.ts +234 -0
  19. package/src/core/colors.ts +32 -0
  20. package/src/core/fonts-digits.ts +12 -0
  21. package/src/core/fonts.ts +13 -0
  22. package/src/core/format.ts +39 -0
  23. package/src/core/i18n.ts +96 -0
  24. package/src/core/index.ts +35 -0
  25. package/src/core/keyboard.ts +229 -0
  26. package/src/core/locales.ts +864 -0
  27. package/src/core/motion.ts +10 -0
  28. package/src/core/renderer.ts +121 -0
  29. package/src/core/state.ts +31 -0
  30. package/src/core/theme.ts +81 -0
  31. package/src/core/types.ts +269 -0
  32. package/src/core/version.ts +3 -0
  33. package/src/ui/html/HtmlRenderer.ts +292 -0
  34. package/src/ui/html/components/BottomBar.ts +240 -0
  35. package/src/ui/html/components/BuyBonus.ts +326 -0
  36. package/src/ui/html/components/GameInfo.ts +384 -0
  37. package/src/ui/html/components/Modal.ts +36 -0
  38. package/src/ui/html/components/ReplayModal.ts +58 -0
  39. package/src/ui/html/components/Settings.ts +59 -0
  40. package/src/ui/html/components/pickers.ts +146 -0
  41. package/src/ui/html/icons-preview.svg +224 -0
  42. package/src/ui/html/icons.ts +31 -0
  43. package/src/ui/html/index.ts +36 -0
  44. package/src/ui/html/motion-dom.ts +29 -0
  45. package/src/ui/html/primitives.ts +85 -0
  46. package/src/ui/html/shell.css.ts +528 -0
  47. package/src/ui/html/theme-css.ts +21 -0
  48. package/src/ui/pixi/PixiRenderer.ts +350 -0
  49. package/src/ui/pixi/components/BottomBar.ts +477 -0
  50. package/src/ui/pixi/components/BuyBonus.ts +763 -0
  51. package/src/ui/pixi/components/GameInfo.ts +559 -0
  52. package/src/ui/pixi/components/Modal.ts +40 -0
  53. package/src/ui/pixi/components/ReplayModal.ts +80 -0
  54. package/src/ui/pixi/components/Settings.ts +117 -0
  55. package/src/ui/pixi/components/pickers.ts +170 -0
  56. package/src/ui/pixi/context.ts +52 -0
  57. package/src/ui/pixi/icons.ts +45 -0
  58. package/src/ui/pixi/index.ts +56 -0
  59. package/src/ui/pixi/motion-pixi.ts +58 -0
  60. package/src/ui/pixi/pixi-icon.ts +119 -0
  61. package/src/ui/pixi/primitives/card.ts +227 -0
  62. package/src/ui/pixi/primitives/controls.ts +243 -0
  63. package/src/ui/pixi/primitives/flex.ts +335 -0
  64. package/src/ui/pixi/primitives/overlay.ts +181 -0
  65. package/src/ui/pixi/primitives/scroll.ts +117 -0
  66. package/src/ui/pixi/primitives/widgets.ts +680 -0
  67. package/src/ui/pixi/text.ts +131 -0
@@ -0,0 +1,39 @@
1
+ import type { CurrencyConfig } from './types';
2
+
3
+ /** The shared money formatter for every shell readout (balance, win, total win, bet, prices).
4
+ *
5
+ * `maxDecimals` is the MAXIMUM fraction digits (default 2); `minDecimals` (defaults to
6
+ * `maxDecimals`) is the MINIMUM. By default the value is shown at exactly `minDecimals` places.
7
+ * With `variableDecimals` — used only for win / total-win — it is rounded to `maxDecimals`, then
8
+ * trailing zeros are trimmed down to (but never past) `minDecimals`, so small wins keep their
9
+ * significant digits. Balance / bet / prices stay fixed at `minDecimals`.
10
+ *
11
+ * Example with `maxDecimals: 4, minDecimals: 2`:
12
+ * fixed → 0.0673 → 0,07 0.3 → 0,30
13
+ * variable → 0.0673 → 0,0673 0.067 → 0,067 0.3 → 0,30 0 → 0,00
14
+ */
15
+ export function formatCurrency(value: number, currency: CurrencyConfig, variableDecimals = false): string {
16
+ const maxDecimals = currency.maxDecimals ?? 2;
17
+ const minDecimals = Math.max(0, Math.min(maxDecimals, currency.minDecimals ?? maxDecimals));
18
+ const thousands = currency.separator?.thousands ?? '.';
19
+ const decimal = currency.separator?.decimal ?? ',';
20
+ const safe = Number.isFinite(value) ? value : 0;
21
+
22
+ // fixed callers round at minDecimals; variable callers round at maxDecimals then trim back down.
23
+ const places = variableDecimals ? maxDecimals : minDecimals;
24
+ const fixed = safe.toFixed(places);
25
+ const [intPart, rawFrac = ''] = fixed.split('.');
26
+ // trim trailing zeros, but keep at least `minDecimals` fraction digits
27
+ let fracPart = rawFrac;
28
+ if (fracPart.length > minDecimals) {
29
+ fracPart = fracPart.replace(/0+$/, '');
30
+ if (fracPart.length < minDecimals) fracPart = fracPart.padEnd(minDecimals, '0');
31
+ }
32
+
33
+ const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousands);
34
+ const number = fracPart.length ? `${grouped}${decimal}${fracPart}` : grouped;
35
+
36
+ return currency.position === 'left'
37
+ ? `${currency.symbol}${number}`
38
+ : `${number} ${currency.symbol}`;
39
+ }
@@ -0,0 +1,96 @@
1
+ import { LOCALES } from './locales';
2
+
3
+ // Social-casino language. English is the source (and, for now, the only) language; `socialize`
4
+ // rewrites the restricted gambling vocabulary into social-safe phrasing while preserving case.
5
+ //
6
+ // Ordering matters: the longest / most specific phrases are listed first so they win over their
7
+ // constituent words (e.g. "buy bonus" before "buy", "pay out" before "pay"). The JS alternation
8
+ // tries entries left-to-right at each position, so a phrase earlier in this list takes priority.
9
+ //
10
+ // Conflicting duplicates in the source table are resolved to a single replacement here
11
+ // (betting→playing, total bet→total play, paid out→won, pays out→win).
12
+ const RULES: ReadonlyArray<readonly [string, string]> = [
13
+ ['be awarded to player’s accounts', 'appear in player’s accounts'],
14
+ ["be awarded to player's accounts", "appear in player's accounts"],
15
+ ['place your bets', 'come and play / join in the game'],
16
+ ['at the cost of', 'for'],
17
+ ['cost of', 'can be played for'],
18
+ ['win feature', 'play feature'],
19
+ ['total bet', 'total play'],
20
+ ['buy bonus', 'get bonus'],
21
+ ['bonus buy', 'bonus / feature'],
22
+ ['pay out', 'win / won'],
23
+ ['paid out', 'won'],
24
+ ['pays out', 'win'],
25
+ ['payout', 'win'], // single word; "pay out" (spaced) is handled above
26
+ ['paytable', 'win table'],
27
+ ['paylines', 'winlines'],
28
+ ['payline', 'winline'],
29
+ ['bet/s', 'play/s'],
30
+ ['betting', 'playing'],
31
+ ['rebet', 'respin'],
32
+ ['stake', 'play amount'],
33
+ ['payer', 'winner'],
34
+ ['bets', 'plays'],
35
+ ['pays', 'wins'],
36
+ ['paid', 'won'],
37
+ ['bought', 'instantly triggered'],
38
+ ['purchase', 'play'],
39
+ ['price', 'play'],
40
+ ['cost', 'play'], // standalone; the "cost of" / "at the cost of" phrases above win first
41
+ ['deposit', 'get coins'],
42
+ ['withdraw', 'redeem'],
43
+ ['currency', 'token'],
44
+ ['gamble', 'play'],
45
+ ['wager', 'play'],
46
+ ['credit', 'balance'],
47
+ ['money', 'coins'],
48
+ ['cash', 'coins'],
49
+ ['fund', 'balance'],
50
+ ['bet', 'play'],
51
+ ['pay', 'win'],
52
+ ['buy', 'play'],
53
+ ];
54
+
55
+ const MAP = new Map(RULES.map(([k, v]) => [k.toLowerCase(), v] as const));
56
+ const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
57
+ // Letter-bounded so we only swap whole words/phrases (e.g. "pay" inside "Autoplay" is left alone).
58
+ const PATTERN = new RegExp(`(?<![A-Za-z])(?:${RULES.map(([k]) => escapeRe(k)).join('|')})(?![A-Za-z])`, 'gi');
59
+
60
+ /** Carry the matched text's capitalisation onto the replacement: ALL CAPS → upper, Capitalised
61
+ * (first letter) → capitalise the replacement's first letter, otherwise lower-case as written. */
62
+ function applyCase(match: string, repl: string): string {
63
+ const letters = match.replace(/[^A-Za-z]/g, '');
64
+ if (letters && letters === letters.toUpperCase()) return repl.toUpperCase();
65
+ if (/^[^A-Za-z]*[A-Z]/.test(match)) return repl.charAt(0).toUpperCase() + repl.slice(1);
66
+ return repl;
67
+ }
68
+
69
+ /** Rewrite restricted gambling terms in `text` to social-safe phrasing, preserving case. */
70
+ export function socialize(text: string): string {
71
+ return text.replace(PATTERN, (m) => {
72
+ const repl = MAP.get(m.toLowerCase());
73
+ return repl == null ? m : applyCase(m, repl);
74
+ });
75
+ }
76
+
77
+ export type Lang = 'de'|'en'|'es'|'fi'|'fr'|'hi'|'id'|'ja'|'ko'|'pl'|'pt'|'ru'|'tr'|'vi'|'zh'|'da';
78
+ export const LANGS: readonly Lang[] = ['de','en','es','fi','fr','hi','id','ja','ko','pl','pt','ru','tr','vi','zh','da'];
79
+ const LANG_SET = new Set<string>(LANGS);
80
+
81
+ export function normalizeLang(code: string | null | undefined): Lang {
82
+ const base = (code ?? '').toLowerCase().split(/[-_]/)[0];
83
+ return (LANG_SET.has(base) ? base : 'en') as Lang;
84
+ }
85
+
86
+ export interface I18nOptions { language: string; isSocial?: boolean; messages?: Partial<Record<Lang, Record<string, string>>>; }
87
+ export interface I18n { readonly lang: Lang; t(src: string): string; }
88
+
89
+ export function createI18n(opts: I18nOptions): I18n {
90
+ const lang = normalizeLang(opts.language);
91
+ const t = (src: string): string => {
92
+ if (lang === 'en') return opts.isSocial ? socialize(src) : src;
93
+ return opts.messages?.[lang]?.[src] ?? LOCALES[lang]?.[src] ?? src;
94
+ };
95
+ return { lang, t };
96
+ }
@@ -0,0 +1,35 @@
1
+ import { ShellController, type CreateShellOptions } from './ShellController';
2
+ import type { ShellSurface, SafeArea } from './renderer';
3
+
4
+ /** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
5
+ * an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
6
+ export type Shell = ShellController & ShellSurface;
7
+
8
+ const NO_INSET: SafeArea = { top: 0, right: 0, bottom: 0, left: 0 };
9
+
10
+ /** Create a shell with an explicit renderer instance (custom or a built-in HtmlRenderer/PixiRenderer).
11
+ * Built-in renderers also have the createGameShell/createPixiShell sugar in /html and /pixi.
12
+ *
13
+ * The returned controller is augmented with the `ShellSurface` facade, delegating to the renderer's
14
+ * optional surface members (inert defaults when it has none) — so any renderer, custom included, is
15
+ * drivable by an embedding host (e.g. game-engine's createSlotGame) without Pixi-specific glue. */
16
+ export function createShell(opts: CreateShellOptions): Shell {
17
+ const controller = new ShellController(opts);
18
+ const r = opts.renderer;
19
+ Object.defineProperties(controller, {
20
+ safeArea: { get: () => r.safeArea ?? NO_INSET, enumerable: true, configurable: true },
21
+ barHeight: { get: () => r.barHeight ?? 0, enumerable: true, configurable: true },
22
+ setVisible: { value: (v: boolean) => r.setVisible?.(v), enumerable: true, configurable: true },
23
+ });
24
+ return controller as Shell;
25
+ }
26
+
27
+ export { ShellController, resolveConfig } from './ShellController';
28
+ export type { CreateShellOptions } from './ShellController';
29
+ export * from './renderer';
30
+ export * from './types';
31
+ export { resolveTheme, SCHEMES, DEFAULT_ACCENT } from './theme';
32
+ export type { ShellTokens } from './theme';
33
+ export { createI18n, socialize, normalizeLang } from './i18n';
34
+ export type { Lang, I18n, I18nOptions } from './i18n';
35
+ export { PACKAGE_VERSION } from './version';
@@ -0,0 +1,229 @@
1
+ import type { ShellState } from './types';
2
+
3
+ export interface KeyboardHost {
4
+ readonly state: ShellState;
5
+ readonly hotkeysEnabled: boolean; // features.hotkeys !== false
6
+ readonly spacebarEnabled: boolean; // features.spacebar !== false
7
+ readonly turboLevels: number; // features.turbo
8
+ readonly autoplayEnabled: boolean; // features.autoplay != null
9
+ readonly buyBonusEnabled: boolean; // features.buyBonus !== false
10
+ hasOpenLayer(): boolean;
11
+ routeToLayer(e: KeyboardEvent): boolean; // give the key to the top layer's onKey; true if consumed
12
+ spin(): void;
13
+ stepBet(dir: 1 | -1): void;
14
+ toggleAutoplay(): void;
15
+ cycleTurbo(): void;
16
+ openBuyBonus(): void;
17
+ openInfo(): void;
18
+ openMenu(): void;
19
+ toggleMute(): void;
20
+ closeLayer(): void;
21
+ }
22
+
23
+ // Bet key detection: bet-up needs Shift for arrow/equal, NumpadAdd is bare; same logic for down.
24
+ // Exported so overlays with their own bet stepper (Buy bonus) honour the SAME keys as the bar.
25
+ export function betDir(e: KeyboardEvent): 1 | -1 | null {
26
+ if (e.code === 'ArrowUp' && e.shiftKey) return 1;
27
+ if (e.code === 'Equal' && e.shiftKey) return 1;
28
+ if (e.code === 'NumpadAdd') return 1;
29
+ if (e.code === 'ArrowDown' && e.shiftKey) return -1;
30
+ if (e.code === 'Minus' && e.shiftKey) return -1;
31
+ if (e.code === 'NumpadSubtract') return -1;
32
+ return null;
33
+ }
34
+
35
+ export class KeyboardController {
36
+ private host: KeyboardHost;
37
+ private doc: Document;
38
+ private spaceHeld = false;
39
+ private holdTimer: ReturnType<typeof setTimeout> | null = null;
40
+ // Bet hold-repeat state
41
+ private betHeldCode: string | null = null;
42
+ private betTimer: ReturnType<typeof setTimeout> | null = null;
43
+
44
+ constructor(host: KeyboardHost, doc?: Document) {
45
+ this.host = host;
46
+ this.doc = doc ?? (typeof document !== 'undefined' ? document : (null as unknown as Document));
47
+ }
48
+
49
+ private isSpinAllowed(): boolean {
50
+ const h = this.host;
51
+ const s = h.state;
52
+ return (
53
+ h.spacebarEnabled &&
54
+ h.hotkeysEnabled &&
55
+ !h.hasOpenLayer() &&
56
+ s.mode === 'base' &&
57
+ !s.autoplay.active
58
+ );
59
+ }
60
+
61
+ private isBetAllowed(): boolean {
62
+ const h = this.host;
63
+ const s = h.state;
64
+ return (
65
+ h.hotkeysEnabled &&
66
+ !h.hasOpenLayer() &&
67
+ s.mode === 'base' &&
68
+ !s.busy
69
+ );
70
+ }
71
+
72
+ private clearBetTimer(): void {
73
+ if (this.betTimer !== null) {
74
+ clearTimeout(this.betTimer);
75
+ this.betTimer = null;
76
+ }
77
+ }
78
+
79
+ private startBetRepeat(dir: 1 | -1, elapsed: number): void {
80
+ // elapsed is ms already spent holding; use it to accelerate toward 45ms floor.
81
+ // Start at 90ms, decrease ~1ms per 10ms held after the first repeat, floor at 45ms.
82
+ const interval = Math.max(45, 90 - Math.floor(elapsed / 10));
83
+ this.betTimer = setTimeout(() => {
84
+ this.betTimer = null;
85
+ if (this.betHeldCode !== null && this.isBetAllowed()) {
86
+ this.host.stepBet(dir);
87
+ this.startBetRepeat(dir, elapsed + interval);
88
+ }
89
+ }, interval);
90
+ }
91
+
92
+ private onKeyDown = (e: KeyboardEvent): void => {
93
+ const target = e.target as HTMLElement | null;
94
+ // Editable element guard — never intercept keyboard input
95
+ if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))) return;
96
+
97
+ // For Space: claim preventDefault early (before layer/mode/busy bail) so the browser's
98
+ // native "Space activates focused button" can't re-fire a shell control and flicker a modal.
99
+ if (e.code === 'Space' && !e.repeat) {
100
+ if (!this.host.spacebarEnabled || !this.host.hotkeysEnabled) return;
101
+ e.preventDefault();
102
+ if (this.host.hasOpenLayer()) {
103
+ this.host.routeToLayer(e);
104
+ return;
105
+ }
106
+ const s = this.host.state;
107
+ if (s.mode !== 'base' || s.busy || s.autoplay.active) return;
108
+ this.spaceHeld = true;
109
+ this.host.spin();
110
+ return;
111
+ }
112
+
113
+ // Bet step keys (Shift+arrows, Shift+=/-, NumpadAdd/Subtract) — non-repeat only
114
+ if (!e.repeat) {
115
+ const dir = betDir(e);
116
+ if (dir !== null && this.isBetAllowed()) {
117
+ this.betHeldCode = e.code;
118
+ this.host.stepBet(dir);
119
+ // First repeat after 350ms initial delay
120
+ this.clearBetTimer();
121
+ const capturedDir = dir;
122
+ this.betTimer = setTimeout(() => {
123
+ this.betTimer = null;
124
+ if (this.betHeldCode !== null && this.isBetAllowed()) {
125
+ this.host.stepBet(capturedDir);
126
+ this.startBetRepeat(capturedDir, 350);
127
+ }
128
+ }, 350);
129
+ return;
130
+ }
131
+ }
132
+
133
+ // Non-Space keys: give the open layer first refusal. If it consumes the key, done; Escape closes
134
+ // it. Anything the layer does NOT consume falls through to the chrome hotkeys below — so the
135
+ // Settings/Info pages still honour Shift+I (Game info), Shift+M (sound), Shift+S, etc.
136
+ if (this.host.hasOpenLayer()) {
137
+ const consumed = this.host.routeToLayer(e);
138
+ if (consumed) return;
139
+ if (e.code === 'Escape') { this.host.closeLayer(); return; }
140
+ // not consumed → fall through to the Shift+letter chrome hotkeys
141
+ }
142
+
143
+ // Shift+letter bar hotkeys — fire when no layer is open, OR when an open layer left the key
144
+ // unconsumed (see fall-through above); gated on hotkeys being enabled.
145
+ if (!e.repeat && e.shiftKey && this.host.hotkeysEnabled) {
146
+ const h = this.host;
147
+ const s = h.state;
148
+ switch (e.code) {
149
+ case 'KeyA':
150
+ if (h.autoplayEnabled && !s.replay) { h.toggleAutoplay(); return; }
151
+ break;
152
+ case 'KeyT':
153
+ if (h.turboLevels > 0 && !s.replay) { h.cycleTurbo(); return; }
154
+ break;
155
+ case 'KeyB':
156
+ if (h.buyBonusEnabled && s.mode === 'base' && !s.replay) { h.openBuyBonus(); return; }
157
+ break;
158
+ case 'KeyI':
159
+ h.openInfo(); return;
160
+ case 'KeyS':
161
+ h.openMenu(); return;
162
+ case 'KeyM':
163
+ h.toggleMute(); return;
164
+ }
165
+ }
166
+ };
167
+
168
+ private onKeyUp = (e: KeyboardEvent): void => {
169
+ if (e.code === 'Space') {
170
+ this.spaceHeld = false;
171
+ this.clearHoldTimer();
172
+ }
173
+ // Stop bet repeat on key release
174
+ if (e.code === this.betHeldCode) {
175
+ this.betHeldCode = null;
176
+ this.clearBetTimer();
177
+ }
178
+ };
179
+
180
+ private onBlur = (): void => {
181
+ // Window blur — stop bet repeat AND hold-to-spin (same as releasing both keys)
182
+ this.betHeldCode = null;
183
+ this.clearBetTimer();
184
+ this.spaceHeld = false;
185
+ this.clearHoldTimer();
186
+ };
187
+
188
+ private clearHoldTimer(): void {
189
+ if (this.holdTimer !== null) {
190
+ clearTimeout(this.holdTimer);
191
+ this.holdTimer = null;
192
+ }
193
+ }
194
+
195
+ attach(): void {
196
+ this.doc.addEventListener('keydown', this.onKeyDown);
197
+ this.doc.addEventListener('keyup', this.onKeyUp);
198
+ // Use window if available for blur events
199
+ if (typeof window !== 'undefined') {
200
+ window.addEventListener('blur', this.onBlur);
201
+ }
202
+ }
203
+
204
+ detach(): void {
205
+ this.doc.removeEventListener('keydown', this.onKeyDown);
206
+ this.doc.removeEventListener('keyup', this.onKeyUp);
207
+ if (typeof window !== 'undefined') {
208
+ window.removeEventListener('blur', this.onBlur);
209
+ }
210
+ this.spaceHeld = false;
211
+ this.clearHoldTimer();
212
+ this.betHeldCode = null;
213
+ this.clearBetTimer();
214
+ }
215
+
216
+ notifyBusyChanged(busy: boolean): void {
217
+ if (busy) return;
218
+ if (!this.spaceHeld) return;
219
+ if (!this.isSpinAllowed()) return;
220
+ // Schedule the next spin after the 120 ms floor (gap between completion and next spin).
221
+ this.clearHoldTimer();
222
+ this.holdTimer = setTimeout(() => {
223
+ this.holdTimer = null;
224
+ if (this.spaceHeld && this.isSpinAllowed()) {
225
+ this.host.spin();
226
+ }
227
+ }, 120);
228
+ }
229
+ }