@energy8platform/platform-core 0.26.1 → 0.28.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.
@@ -1,229 +0,0 @@
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
- }