@energy8platform/platform-core 0.20.0 → 0.21.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/README.md +194 -0
- package/dist/index.cjs.js +1940 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +302 -2
- package/dist/index.esm.js +1938 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/shell.cjs.js +1993 -0
- package/dist/shell.cjs.js.map +1 -0
- package/dist/shell.d.ts +320 -0
- package/dist/shell.esm.js +1989 -0
- package/dist/shell.esm.js.map +1 -0
- package/package.json +6 -1
- package/scripts/build-shell-font.mjs +64 -0
- package/src/index.ts +16 -0
- package/src/shell/GameShell.ts +294 -0
- package/src/shell/INTER-LICENSE.txt +93 -0
- package/src/shell/colors.ts +32 -0
- package/src/shell/components/BottomBar.ts +217 -0
- package/src/shell/components/BuyBonus.ts +163 -0
- package/src/shell/components/GameInfo.ts +253 -0
- package/src/shell/components/Modal.ts +36 -0
- package/src/shell/components/ReplayModal.ts +56 -0
- package/src/shell/components/Settings.ts +60 -0
- package/src/shell/components/icons.ts +40 -0
- package/src/shell/components/pickers.ts +76 -0
- package/src/shell/components/primitives.ts +84 -0
- package/src/shell/fonts.ts +13 -0
- package/src/shell/format.ts +36 -0
- package/src/shell/i18n.ts +67 -0
- package/src/shell/index.ts +20 -0
- package/src/shell/motion.ts +43 -0
- package/src/shell/shell.css.ts +371 -0
- package/src/shell/state.ts +30 -0
- package/src/shell/theme.ts +56 -0
- package/src/shell/types.ts +191 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { SHELL_FONT_CSS } from './fonts';
|
|
2
|
+
|
|
3
|
+
export const SHELL_ROOT_ID = '__ge-game-shell__';
|
|
4
|
+
|
|
5
|
+
// Inter (bundled, base64) leads the stack so the shell renders identically on every
|
|
6
|
+
// platform; the system fonts stay as graceful fallback if the webfont ever fails to load.
|
|
7
|
+
export const SHELL_CSS = SHELL_FONT_CSS + `
|
|
8
|
+
#${SHELL_ROOT_ID} {
|
|
9
|
+
position: absolute; inset: 0;
|
|
10
|
+
pointer-events: none; z-index: 9000;
|
|
11
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
12
|
+
color: var(--shell-fg);
|
|
13
|
+
}
|
|
14
|
+
#${SHELL_ROOT_ID} svg { display:block; }
|
|
15
|
+
|
|
16
|
+
/* floating readouts (no panel) */
|
|
17
|
+
#${SHELL_ROOT_ID} .ge-rd { font-weight:700; font-size:13px; line-height:1; white-space:nowrap;
|
|
18
|
+
text-shadow:0 1px 3px rgba(0,0,0,.65); font-variant-numeric:tabular-nums; }
|
|
19
|
+
#${SHELL_ROOT_ID} .ge-rd .ge-lbl { display:block; color:var(--shell-muted); font-weight:600;
|
|
20
|
+
font-size:9px; letter-spacing:.1em; text-transform:uppercase; margin-bottom:4px; text-shadow:none; }
|
|
21
|
+
|
|
22
|
+
/* icon buttons (borderless) */
|
|
23
|
+
#${SHELL_ROOT_ID} .ge-iconbtn { pointer-events:auto; cursor:pointer; border:none; background:none;
|
|
24
|
+
padding:0; color:var(--shell-icon); width:40px; height:40px; display:flex; align-items:center;
|
|
25
|
+
justify-content:center; font-size:24px; position:relative; transition:transform .08s ease; }
|
|
26
|
+
#${SHELL_ROOT_ID} .ge-iconbtn:active { transform:scale(.92); }
|
|
27
|
+
#${SHELL_ROOT_ID} .ge-iconbtn[disabled] { opacity:.35; cursor:default; }
|
|
28
|
+
#${SHELL_ROOT_ID} .ge-iconbtn.ge-active { color:var(--shell-icon-active); }
|
|
29
|
+
|
|
30
|
+
/* SPIN — white disc by default, big glyph reaching for the rim; lights up accent on hover */
|
|
31
|
+
#${SHELL_ROOT_ID} .ge-shell-spin { pointer-events:auto; cursor:pointer; border:3px solid #000; border-radius:50%;
|
|
32
|
+
width:86px; height:86px; background:var(--shell-spin); color:var(--shell-spin-fg); box-sizing:border-box; font-size:68px;
|
|
33
|
+
display:flex; align-items:center; justify-content:center;
|
|
34
|
+
transition:transform .08s ease, box-shadow .12s ease, background .12s ease, color .12s ease; }
|
|
35
|
+
#${SHELL_ROOT_ID} .ge-shell-spin:hover { background:var(--shell-accent); color:#fff;
|
|
36
|
+
box-shadow:0 0 0 3px var(--shell-accent), 0 0 18px 2px var(--shell-accent); }
|
|
37
|
+
#${SHELL_ROOT_ID} .ge-shell-spin:active { transform:scale(.94); }
|
|
38
|
+
/* disabled: dim via filter (stays opaque) so the plaque seam doesn't show through the disc */
|
|
39
|
+
#${SHELL_ROOT_ID} .ge-shell-spin[disabled] { filter:grayscale(.4) brightness(.62); box-shadow:none; cursor:default; }
|
|
40
|
+
/* spinning while a spin is in progress */
|
|
41
|
+
#${SHELL_ROOT_ID} .ge-shell-spin.ge-spinning svg { transform-origin:50% 50%; animation:ge-spin-rot .8s linear infinite; }
|
|
42
|
+
@keyframes ge-spin-rot { to { transform:rotate(360deg); } }
|
|
43
|
+
/* autoplay running: STOP glyph + countdown stacked in the disc */
|
|
44
|
+
#${SHELL_ROOT_ID} .ge-shell-spin.ge-stop { position:relative; }
|
|
45
|
+
#${SHELL_ROOT_ID} .ge-spin-stop { display:flex; } /* STOP glyph at the disc's full size, like SPIN */
|
|
46
|
+
/* no entrance animation: the bar re-renders many times per spin, so any animation here
|
|
47
|
+
would replay each time and make the counter visibly jump. tabular-nums keeps it steady. */
|
|
48
|
+
#${SHELL_ROOT_ID} .ge-spin-count { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
|
49
|
+
font-size:22px; font-weight:800; line-height:1; font-variant-numeric:tabular-nums; }
|
|
50
|
+
|
|
51
|
+
/* BUY BONUS — round accent badge, 2-line label, text pulses + accent glow on hover */
|
|
52
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus { pointer-events:auto; cursor:pointer; box-sizing:border-box;
|
|
53
|
+
width:80px; height:80px; border-radius:50%; border:3px solid #000; background:var(--shell-buybonus);
|
|
54
|
+
color:#fff; font-weight:800; letter-spacing:.02em; font-size:13px; line-height:1.08; text-align:center;
|
|
55
|
+
display:flex; align-items:center; justify-content:center; transition:transform .08s ease, box-shadow .12s ease; }
|
|
56
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus span { display:inline-block; will-change:transform; }
|
|
57
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus:hover { box-shadow:0 0 0 3px var(--shell-accent), 0 0 16px 1px var(--shell-accent); }
|
|
58
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus:hover span { animation:ge-bb-pulse .7s ease-in-out infinite; }
|
|
59
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus:active { transform:scale(.96); }
|
|
60
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus[disabled] { filter:grayscale(.5) brightness(.72); box-shadow:none; cursor:default; }
|
|
61
|
+
#${SHELL_ROOT_ID} .ge-shell-buybonus[disabled] span { animation:none; }
|
|
62
|
+
@keyframes ge-bb-pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.16)} }
|
|
63
|
+
|
|
64
|
+
/* host = bottom-anchored flex column: [win pill (on overflow)] above [the bar] */
|
|
65
|
+
#${SHELL_ROOT_ID} .ge-shell-barhost { position:absolute; left:0; right:0; bottom:0; pointer-events:none;
|
|
66
|
+
display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:8px;
|
|
67
|
+
transform-origin:bottom center; }
|
|
68
|
+
/* bottom bar: transparent, two zones (wide default) */
|
|
69
|
+
#${SHELL_ROOT_ID} .ge-shell-bottom { width:100%; box-sizing:border-box; pointer-events:none;
|
|
70
|
+
display:flex; align-items:center; justify-content:space-between; padding:0 18px 14px; gap:14px; }
|
|
71
|
+
#${SHELL_ROOT_ID} .ge-zone { display:flex; align-items:center; gap:14px; pointer-events:none; }
|
|
72
|
+
#${SHELL_ROOT_ID} .ge-zone > * { pointer-events:auto; }
|
|
73
|
+
#${SHELL_ROOT_ID} .ge-betstep { display:flex; flex-direction:column; gap:2px; }
|
|
74
|
+
/* auto stacked over turbo (far-right column, base/desktop) */
|
|
75
|
+
#${SHELL_ROOT_ID} .ge-autoturbo { display:flex; flex-direction:column; gap:2px; }
|
|
76
|
+
#${SHELL_ROOT_ID} .ge-autoturbo .ge-iconbtn { width:40px; height:30px; }
|
|
77
|
+
|
|
78
|
+
/* mobile (portrait) — full-width stacked plaques: [balance · win] · [controls] · [− bet +] */
|
|
79
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-shell-bottom { flex-direction:column; align-items:center; gap:14px; padding:8px 12px 8px; }
|
|
80
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-top { width:100%; height:46px; justify-content:space-between; }
|
|
81
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-controls { display:flex; align-items:center; justify-content:space-between;
|
|
82
|
+
width:100%; box-sizing:border-box; height:62px; border-radius:18px; padding:0 18px; }
|
|
83
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-iconbtn,
|
|
84
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-rd,
|
|
85
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-rd .ge-lbl { color:#fff; }
|
|
86
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-rd { text-shadow:none; text-align:center; }
|
|
87
|
+
/* mobile: restore accent hover + autoplay glow (the white rule above out-specifies the base ones) */
|
|
88
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-iconbtn:hover,
|
|
89
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-controls .ge-iconbtn.ge-glow { color:var(--shell-accent); }
|
|
90
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-m-bet { width:100%; height:46px; padding:0 18px; gap:8px; justify-content:space-between; }
|
|
91
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-shell-spin { width:84px; height:84px; font-size:66px; }
|
|
92
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-shell-buybonus { width:50px; height:50px; font-size:9px; border-width:2px; }
|
|
93
|
+
/* landscape overflow — size the column to content, centre it; JS scales the whole stack to fit */
|
|
94
|
+
#${SHELL_ROOT_ID} .ge-shell-barhost.ge-fit { left:50%; right:auto; width:max-content; max-width:none; }
|
|
95
|
+
#${SHELL_ROOT_ID} .ge-shell-barhost.ge-fit .ge-shell-bottom { width:max-content; }
|
|
96
|
+
|
|
97
|
+
/* full-screen overlays — header (title left, prominent X/back), scrolling body, vh-clamped for popout/mobile.
|
|
98
|
+
Frosted backdrop: the game shows through, blurred, behind a light dark tint (white-on-blur).
|
|
99
|
+
Same glass "plaque" language as the control bar; scheme-independent. */
|
|
100
|
+
#${SHELL_ROOT_ID} .ge-shell-overlay { position:absolute; inset:0; z-index:50; pointer-events:auto;
|
|
101
|
+
background:rgba(12,17,28,.5); backdrop-filter:blur(20px) saturate(120%);
|
|
102
|
+
-webkit-backdrop-filter:blur(20px) saturate(120%);
|
|
103
|
+
display:flex; flex-direction:column; animation:ge-ov-in .16s ease-out; }
|
|
104
|
+
/* SPIN/BUY BONUS use z-index:3 to overlap their plaques; lift the overlay clear above them */
|
|
105
|
+
@keyframes ge-ov-in { from { opacity:0; } to { opacity:1; } }
|
|
106
|
+
#${SHELL_ROOT_ID} .ge-ov-head { flex:0 0 auto; display:flex; align-items:center; gap:8px; padding:6px 10px; }
|
|
107
|
+
#${SHELL_ROOT_ID} .ge-ov-title { flex:1; margin:0; text-align:center; color:#fff; font-weight:800; letter-spacing:.04em;
|
|
108
|
+
text-transform:uppercase; font-size:clamp(13px,2.6vh,16px); }
|
|
109
|
+
#${SHELL_ROOT_ID} .ge-ov-spacer { flex:0 0 auto; width:32px; }
|
|
110
|
+
#${SHELL_ROOT_ID} .ge-ov-nav { flex:0 0 auto; display:flex; align-items:center; justify-content:center;
|
|
111
|
+
width:32px; height:32px; border-radius:9px; cursor:pointer; color:#fff;
|
|
112
|
+
background:var(--shell-plaque-dark); border:none; font-size:18px;
|
|
113
|
+
transition:background .12s ease, color .12s ease; }
|
|
114
|
+
#${SHELL_ROOT_ID} .ge-ov-nav:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
|
|
115
|
+
#${SHELL_ROOT_ID} .ge-ov-scroll { flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden; }
|
|
116
|
+
#${SHELL_ROOT_ID} .ge-ov-body { max-width:800px; margin:0 auto; box-sizing:border-box;
|
|
117
|
+
padding:clamp(6px,2vh,16px) clamp(16px,4vw,24px) clamp(16px,4vh,28px); }
|
|
118
|
+
|
|
119
|
+
/* full-width overlay rows — glass plaque, white-on-dark */
|
|
120
|
+
#${SHELL_ROOT_ID} .ge-ov-row { display:flex; align-items:center; gap:12px; width:100%; box-sizing:border-box;
|
|
121
|
+
padding:clamp(11px,2.2vh,15px) 16px; margin-bottom:10px; border:none;
|
|
122
|
+
border-radius:16px; background:var(--shell-plaque-glass); color:#fff; font-size:14px; font-weight:600; }
|
|
123
|
+
#${SHELL_ROOT_ID} .ge-ov-row .ge-grow { flex:1; text-align:left; }
|
|
124
|
+
#${SHELL_ROOT_ID} button.ge-ov-row { cursor:pointer; font-family:inherit; transition:background .12s ease, color .12s ease; }
|
|
125
|
+
#${SHELL_ROOT_ID} button.ge-ov-row:hover { background:var(--shell-plaque-glass-hover); color:var(--shell-accent); }
|
|
126
|
+
#${SHELL_ROOT_ID} .ge-ov-row.ge-col { flex-direction:column; align-items:stretch; gap:10px; }
|
|
127
|
+
#${SHELL_ROOT_ID} .ge-ov-row .ge-row-head { display:flex; justify-content:space-between; align-items:center; }
|
|
128
|
+
#${SHELL_ROOT_ID} .ge-ov-row .ge-row-head .ge-val { color:var(--shell-plaque-label); font-variant-numeric:tabular-nums; font-weight:700; }
|
|
129
|
+
#${SHELL_ROOT_ID} .ge-slider { width:100%; accent-color:var(--shell-accent); }
|
|
130
|
+
#${SHELL_ROOT_ID} .ge-toggle { flex:0 0 auto; width:42px; height:24px; border-radius:999px; background:var(--shell-plaque-line);
|
|
131
|
+
border:none; cursor:pointer; position:relative; }
|
|
132
|
+
#${SHELL_ROOT_ID} .ge-toggle.ge-on { background:var(--shell-accent); }
|
|
133
|
+
#${SHELL_ROOT_ID} .ge-toggle i { position:absolute; top:2px; left:2px; width:20px; height:20px; border-radius:50%;
|
|
134
|
+
background:#fff; transition:left .12s ease; }
|
|
135
|
+
#${SHELL_ROOT_ID} .ge-toggle.ge-on i { left:20px; }
|
|
136
|
+
/* sound on/off — speaker icon button (replaces the toggle) */
|
|
137
|
+
#${SHELL_ROOT_ID} .ge-snd { pointer-events:auto; cursor:pointer; border:none; background:none; padding:0;
|
|
138
|
+
width:36px; height:36px; display:flex; align-items:center; justify-content:center; font-size:24px;
|
|
139
|
+
color:#fff; transition:color .12s ease, transform .08s ease; }
|
|
140
|
+
#${SHELL_ROOT_ID} .ge-snd:not(.ge-active) { color:var(--shell-plaque-label); }
|
|
141
|
+
#${SHELL_ROOT_ID} .ge-snd:hover { color:var(--shell-accent); }
|
|
142
|
+
#${SHELL_ROOT_ID} .ge-snd:active { transform:scale(.92); }
|
|
143
|
+
|
|
144
|
+
/* game info — each section is its own glass plaque; body text sized for comfortable reading */
|
|
145
|
+
#${SHELL_ROOT_ID} .ge-gi-sec { margin-bottom:12px; background:var(--shell-plaque-glass);
|
|
146
|
+
border-radius:16px; padding:16px 18px; }
|
|
147
|
+
#${SHELL_ROOT_ID} .ge-gi-sec h3 { color:var(--shell-plaque-label); font-size:11px; letter-spacing:.14em;
|
|
148
|
+
text-transform:uppercase; margin:0 0 12px; }
|
|
149
|
+
#${SHELL_ROOT_ID} .ge-gi-sec p { color:rgba(255,255,255,.88); font-size:15px; line-height:1.6; margin:0; }
|
|
150
|
+
|
|
151
|
+
/* controls — two blocks (gameplay / menu & info), icon/name/description per control */
|
|
152
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-block + .ge-gi-ctl-block { margin-top:16px; padding-top:4px; border-top:1px solid var(--shell-plaque-line); }
|
|
153
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-block-h { color:var(--shell-plaque-label); font-size:11px; letter-spacing:.12em;
|
|
154
|
+
text-transform:uppercase; margin:8px 0 2px; font-weight:700; }
|
|
155
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl { display:flex; align-items:center; gap:14px; padding:9px 0; }
|
|
156
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl + .ge-gi-ctl { border-top:1px solid var(--shell-plaque-line); }
|
|
157
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-ic { flex:0 0 auto; width:48px; height:48px; display:flex; align-items:center;
|
|
158
|
+
justify-content:center; font-size:26px; color:#fff; }
|
|
159
|
+
/* bet shows both chevrons, stacked & smaller */
|
|
160
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-ic--bet { flex-direction:column; font-size:18px; gap:0; }
|
|
161
|
+
/* buy bonus draws the real control-bar badge, scaled down & non-interactive */
|
|
162
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-ic .ge-shell-buybonus { width:46px; height:46px; font-size:8px; border-width:2px; pointer-events:none; }
|
|
163
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-tx { display:flex; flex-direction:column; gap:2px; }
|
|
164
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-tx b { color:#fff; font-size:15px; font-weight:700; }
|
|
165
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-tx span { color:rgba(255,255,255,.7); font-size:13px; line-height:1.4; }
|
|
166
|
+
|
|
167
|
+
/* paytable — cards: symbol image on top, name, then win tiers "<count> x<mult>" */
|
|
168
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(120px,1fr)); gap:10px; }
|
|
169
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-card { display:flex; flex-direction:column; align-items:center; gap:8px;
|
|
170
|
+
background:var(--shell-plaque-dark); border-radius:14px; padding:14px 12px; }
|
|
171
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-sym { display:flex; flex-direction:column; align-items:center; gap:5px; min-width:0; }
|
|
172
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-sym img { width:56px; height:56px; object-fit:contain; }
|
|
173
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-sym span { color:#fff; font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; }
|
|
174
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-wins { display:flex; flex-direction:column; align-items:stretch; gap:2px; width:100%; }
|
|
175
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-win { display:flex; justify-content:space-between; align-items:baseline; gap:14px;
|
|
176
|
+
font-size:13.5px; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
177
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-win i { color:var(--shell-plaque-label); font-style:normal; }
|
|
178
|
+
#${SHELL_ROOT_ID} .ge-gi-pt-win b { color:var(--shell-accent); font-weight:700; }
|
|
179
|
+
|
|
180
|
+
/* wins — accent-filled mini-grid(s); classic = one per line, cluster/anywhere = one, ways = two */
|
|
181
|
+
#${SHELL_ROOT_ID} .ge-gi-pl-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(72px,1fr)); gap:12px; }
|
|
182
|
+
#${SHELL_ROOT_ID} .ge-gi-pl-item { display:flex; flex-direction:column; align-items:center; gap:6px; }
|
|
183
|
+
#${SHELL_ROOT_ID} .ge-gi-pl-svg { width:100%; height:auto; max-width:140px; }
|
|
184
|
+
#${SHELL_ROOT_ID} .ge-gi-pl-cell { fill:var(--shell-plaque-line); }
|
|
185
|
+
#${SHELL_ROOT_ID} .ge-gi-pl-on { fill:var(--shell-accent); }
|
|
186
|
+
#${SHELL_ROOT_ID} .ge-gi-pl-cap { color:#fff; font-size:13px; font-weight:700; }
|
|
187
|
+
#${SHELL_ROOT_ID} .ge-gi-win-row { display:flex; align-items:flex-start; gap:16px; flex-wrap:wrap; }
|
|
188
|
+
#${SHELL_ROOT_ID} .ge-gi-win-row .ge-gi-pl-svg { flex:0 0 auto; width:140px; }
|
|
189
|
+
#${SHELL_ROOT_ID} .ge-gi-win-desc { flex:1; min-width:160px; color:rgba(255,255,255,.78); font-size:14px; line-height:1.5; margin:0; }
|
|
190
|
+
#${SHELL_ROOT_ID} .ge-gi-win-two { display:flex; gap:22px; flex-wrap:wrap; }
|
|
191
|
+
#${SHELL_ROOT_ID} .ge-gi-win-col { display:flex; flex-direction:column; align-items:center; gap:6px; }
|
|
192
|
+
#${SHELL_ROOT_ID} .ge-gi-win-col .ge-gi-pl-svg { width:140px; }
|
|
193
|
+
#${SHELL_ROOT_ID} .ge-gi-win-tag { font-size:12px; font-weight:700; }
|
|
194
|
+
#${SHELL_ROOT_ID} .ge-gi-win-ok { color:#4ade80; }
|
|
195
|
+
#${SHELL_ROOT_ID} .ge-gi-win-no { color:#f87171; }
|
|
196
|
+
#${SHELL_ROOT_ID} .ge-gi-win-badge { display:inline-block; margin-left:10px; padding:2px 8px; border-radius:999px;
|
|
197
|
+
font-size:11px; font-weight:700; letter-spacing:.02em; color:var(--shell-accent);
|
|
198
|
+
background:var(--shell-plaque-dark); vertical-align:middle; }
|
|
199
|
+
|
|
200
|
+
/* modes — comparison cards */
|
|
201
|
+
#${SHELL_ROOT_ID} .ge-gi-modes { display:flex; flex-direction:column; }
|
|
202
|
+
#${SHELL_ROOT_ID} .ge-gi-mode { padding:12px 0; }
|
|
203
|
+
#${SHELL_ROOT_ID} .ge-gi-mode + .ge-gi-mode { border-top:1px solid var(--shell-plaque-line); }
|
|
204
|
+
#${SHELL_ROOT_ID} .ge-gi-mode-top { display:flex; flex-wrap:wrap; align-items:baseline; justify-content:space-between; gap:6px 16px; margin-bottom:6px; }
|
|
205
|
+
#${SHELL_ROOT_ID} .ge-gi-mode-h { color:#fff; font-size:16px; font-weight:800; }
|
|
206
|
+
#${SHELL_ROOT_ID} .ge-gi-mode-stats { display:flex; flex-wrap:wrap; gap:14px; }
|
|
207
|
+
#${SHELL_ROOT_ID} .ge-gi-mode-st { display:flex; align-items:baseline; gap:5px; }
|
|
208
|
+
#${SHELL_ROOT_ID} .ge-gi-mode-st span { color:var(--shell-plaque-label); font-size:10px; letter-spacing:.1em; text-transform:uppercase; }
|
|
209
|
+
#${SHELL_ROOT_ID} .ge-gi-mode-st b { color:#fff; font-size:14px; font-weight:800; font-variant-numeric:tabular-nums; }
|
|
210
|
+
#${SHELL_ROOT_ID} .ge-gi-mode-desc { color:rgba(255,255,255,.78); font-size:14px; line-height:1.5; margin:0; }
|
|
211
|
+
#${SHELL_ROOT_ID} .ge-gi-custom { color:rgba(255,255,255,.88); font-size:15px; line-height:1.6; }
|
|
212
|
+
|
|
213
|
+
/* buy bonus cards — art-forward, centred, flat (no gradients); --card-acc/--card-ink per card.
|
|
214
|
+
order: title → thumb → description → (spacer) → volatility → price → full-bleed button. */
|
|
215
|
+
/* wide: horizontal scrolling strip. Cards scale PROPORTIONALLY with the viewport — every
|
|
216
|
+
dimension is in em-units, so the single font-size knob shrinks the whole card uniformly
|
|
217
|
+
(like the control bar's fit-scale), keeping small popouts readable. mobile: vertical stack. */
|
|
218
|
+
/* the buy-bonus scroll area is a SIZE CONTAINER, so the cards' cqh units measure the overlay
|
|
219
|
+
(the popout frame) and not the browser window — cards fit without any vertical scroll. */
|
|
220
|
+
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-scroll { container-type:size; }
|
|
221
|
+
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { padding:clamp(8px,3cqh,16px); }
|
|
222
|
+
#${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
|
|
223
|
+
scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
|
|
224
|
+
/* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
|
|
225
|
+
browser window, so cards shrink to fit the real container height. */
|
|
226
|
+
#${SHELL_ROOT_ID} .ge-bb-grid .ge-bonus-card { flex:0 0 18.5em; scroll-snap-align:start;
|
|
227
|
+
font-size:clamp(7px, 4cqh, 13px); }
|
|
228
|
+
/* mobile: vertical stack at a fixed, readable size — scroll the list, don't shrink the cards */
|
|
229
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid { display:flex; flex-direction:column; gap:14px; overflow:visible; }
|
|
230
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid .ge-bonus-card { flex:0 0 auto; font-size:13px; }
|
|
231
|
+
#${SHELL_ROOT_ID} .ge-bonus-card { display:flex; flex-direction:column; border-radius:1.4em; overflow:hidden;
|
|
232
|
+
background:var(--shell-plaque-glass); border:1px solid var(--shell-plaque-line); color:#fff; text-align:center;
|
|
233
|
+
pointer-events:auto; cursor:pointer; transition:box-shadow .12s ease, background .12s ease; }
|
|
234
|
+
#${SHELL_ROOT_ID} .ge-bonus-card:hover:not(.ge-bonus-off) {
|
|
235
|
+
box-shadow:0 0 0 1px var(--card-acc), 0 12px 34px -12px var(--card-acc); }
|
|
236
|
+
#${SHELL_ROOT_ID} .ge-bonus-body { display:flex; flex-direction:column; align-items:center; flex:1; padding:1.25em 1.1em .9em; }
|
|
237
|
+
#${SHELL_ROOT_ID} .ge-bonus-title { font-size:1.3em; font-weight:800; letter-spacing:.04em; text-transform:uppercase;
|
|
238
|
+
color:var(--card-acc); margin-bottom:.75em; }
|
|
239
|
+
#${SHELL_ROOT_ID} .ge-bonus-thumb { height:6.2em; display:flex; align-items:center; justify-content:center; margin-bottom:.7em; }
|
|
240
|
+
#${SHELL_ROOT_ID} .ge-bonus-thumb img { max-height:100%; max-width:100%; object-fit:contain; filter:drop-shadow(0 6px 14px rgba(0,0,0,.45)); }
|
|
241
|
+
#${SHELL_ROOT_ID} .ge-bonus-thumb-ph { font-size:3.5em; color:var(--card-acc); opacity:.85; display:flex; }
|
|
242
|
+
#${SHELL_ROOT_ID} .ge-bonus-desc { font-size:.96em; line-height:1.45; color:rgba(255,255,255,.82); }
|
|
243
|
+
#${SHELL_ROOT_ID} .ge-bonus-spacer { flex:1; min-height:.7em; }
|
|
244
|
+
#${SHELL_ROOT_ID} .ge-bonus-vol { display:flex; justify-content:center; align-items:center; gap:0;
|
|
245
|
+
font-size:2.1em; line-height:1; margin-bottom:.55em; }
|
|
246
|
+
#${SHELL_ROOT_ID} .ge-bonus-vol-on, #${SHELL_ROOT_ID} .ge-bonus-vol-off { display:inline-flex; gap:0; }
|
|
247
|
+
#${SHELL_ROOT_ID} .ge-bonus-vol-on { color:var(--card-acc); }
|
|
248
|
+
#${SHELL_ROOT_ID} .ge-bonus-vol-off { color:rgba(255,255,255,.18); }
|
|
249
|
+
#${SHELL_ROOT_ID} .ge-bonus-price { font-size:1.6em; font-weight:800; color:#fff; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
250
|
+
#${SHELL_ROOT_ID} .ge-bonus-cta { width:100%; border:none; padding:1.15em; font-weight:800; font-size:1.05em;
|
|
251
|
+
letter-spacing:.05em; text-transform:uppercase; cursor:pointer; background:var(--card-acc); color:var(--card-ink);
|
|
252
|
+
transition:filter .12s ease; }
|
|
253
|
+
#${SHELL_ROOT_ID} .ge-bonus-cta:hover:not([disabled]) { filter:brightness(1.06); }
|
|
254
|
+
/* unaffordable / disabled */
|
|
255
|
+
#${SHELL_ROOT_ID} .ge-bonus-card.ge-bonus-off { opacity:.62; cursor:default; }
|
|
256
|
+
#${SHELL_ROOT_ID} .ge-bonus-off .ge-bonus-title { color:rgba(255,255,255,.6); }
|
|
257
|
+
#${SHELL_ROOT_ID} .ge-bonus-off .ge-bonus-vol-on { color:rgba(255,255,255,.4); }
|
|
258
|
+
#${SHELL_ROOT_ID} .ge-bonus-off .ge-bonus-cta { background:#8d939e; color:#3a3f47; cursor:default; }
|
|
259
|
+
/* buy-bonus confirm — bonus preview inside the shared sheet card (see .ge-sheet-actions below) */
|
|
260
|
+
#${SHELL_ROOT_ID} .ge-confirm-preview { display:flex; flex-direction:column; align-items:center; gap:10px; text-align:center; }
|
|
261
|
+
#${SHELL_ROOT_ID} .ge-confirm-preview .ge-bonus-thumb,
|
|
262
|
+
#${SHELL_ROOT_ID} .ge-confirm-preview .ge-bonus-vol { margin:0; }
|
|
263
|
+
/* effective-bet readout while a feature is active (value colour set inline to the feature accent) */
|
|
264
|
+
#${SHELL_ROOT_ID} .ge-rd.ge-bet-feature { font-weight:800; }
|
|
265
|
+
/* bet control — compact pill in a thin footer at the screen bottom (footer only as tall as the pill) */
|
|
266
|
+
#${SHELL_ROOT_ID} .ge-bb-betbar { flex:0 0 auto; display:flex; justify-content:center; padding:4px; }
|
|
267
|
+
#${SHELL_ROOT_ID} .ge-bb-betpill { display:inline-flex; align-items:center; gap:4px;
|
|
268
|
+
background:var(--shell-plaque-dark); border-radius:999px; padding:3px 5px; }
|
|
269
|
+
#${SHELL_ROOT_ID} .ge-bb-betstep { pointer-events:auto; cursor:pointer; width:32px; height:32px; border:none; border-radius:50%;
|
|
270
|
+
background:none; color:#fff; font-size:20px; display:flex; align-items:center; justify-content:center;
|
|
271
|
+
transition:color .12s ease, transform .08s ease; }
|
|
272
|
+
#${SHELL_ROOT_ID} .ge-bb-betstep:hover { color:var(--shell-accent); }
|
|
273
|
+
#${SHELL_ROOT_ID} .ge-bb-betstep:active { transform:scale(.9); }
|
|
274
|
+
#${SHELL_ROOT_ID} .ge-bb-betval { min-width:80px; text-align:center; padding:0 2px; }
|
|
275
|
+
#${SHELL_ROOT_ID} .ge-bb-betval span { display:block; font-size:7px; font-weight:600; letter-spacing:.14em; text-transform:uppercase;
|
|
276
|
+
color:var(--shell-plaque-label); }
|
|
277
|
+
#${SHELL_ROOT_ID} .ge-bb-betval b { font-size:14px; font-weight:800; font-variant-numeric:tabular-nums; color:#fff; }
|
|
278
|
+
|
|
279
|
+
/* ═══ base/wide plaque bar — grouped dark + glass panels (reference-style) ═══ */
|
|
280
|
+
#${SHELL_ROOT_ID} .ge-zone-plaques { gap:0; } /* panels connect; buttons overlap */
|
|
281
|
+
#${SHELL_ROOT_ID} .ge-pl { display:flex; align-items:center; height:56px; box-sizing:border-box;
|
|
282
|
+
border-radius:16px; padding:0 20px; gap:18px; }
|
|
283
|
+
#${SHELL_ROOT_ID} .ge-pl-dark { background:var(--shell-plaque-dark); }
|
|
284
|
+
#${SHELL_ROOT_ID} .ge-pl-glass { background:var(--shell-plaque-glass); }
|
|
285
|
+
/* FS spins-counter plaque (wide) — sits between balance and bet, glass like balance */
|
|
286
|
+
#${SHELL_ROOT_ID} .ge-fscount { justify-content:center; min-width:150px; }
|
|
287
|
+
#${SHELL_ROOT_ID} .ge-pl .ge-rd { color:#fff; text-shadow:none; }
|
|
288
|
+
#${SHELL_ROOT_ID} .ge-pl .ge-rd .ge-lbl { color:var(--shell-plaque-label); }
|
|
289
|
+
#${SHELL_ROOT_ID} .ge-pl .ge-iconbtn { color:#fff; }
|
|
290
|
+
/* LEFT: [menu] ⊐ coin ⊏ [balance] — coin overlaps both; balance fixed-wide so it doesn't jiggle */
|
|
291
|
+
#${SHELL_ROOT_ID} .ge-pl-menu { border-radius:16px 0 0 16px; padding-right:20px; }
|
|
292
|
+
#${SHELL_ROOT_ID} .ge-pl-bal { border-radius:0 16px 16px 0; padding-left:24px; min-width:240px; }
|
|
293
|
+
#${SHELL_ROOT_ID} .ge-zone-plaques .ge-shell-buybonus { margin:0 -16px; position:relative; z-index:3; }
|
|
294
|
+
/* RIGHT: [bet] · |divider| · [auto · SPIN · turbo] */
|
|
295
|
+
#${SHELL_ROOT_ID} .ge-pl-bet { border-radius:16px 0 0 16px; justify-content:space-between;
|
|
296
|
+
width:210px; padding-right:8px; } /* fixed: bet value never reflows the panel */
|
|
297
|
+
#${SHELL_ROOT_ID} .ge-pl-divider { align-self:center; flex:0 0 auto; width:1px; height:30px;
|
|
298
|
+
background:var(--shell-plaque-line); }
|
|
299
|
+
#${SHELL_ROOT_ID} .ge-spinwrap { display:flex; align-items:center; gap:10px; height:56px;
|
|
300
|
+
border-radius:0 16px 16px 0; padding:0 8px 0 14px; }
|
|
301
|
+
#${SHELL_ROOT_ID} .ge-spinwrap .ge-iconbtn { color:#fff; }
|
|
302
|
+
#${SHELL_ROOT_ID} .ge-spinwrap .ge-shell-spin { margin:0 -2px; position:relative; z-index:3; }
|
|
303
|
+
/* tighter bet chevrons, parked next to autoplay */
|
|
304
|
+
#${SHELL_ROOT_ID} .ge-pl-bet .ge-betstep { gap:0; }
|
|
305
|
+
#${SHELL_ROOT_ID} .ge-pl-bet .ge-betstep .ge-iconbtn { height:24px; }
|
|
306
|
+
/* accent hover highlight on every control */
|
|
307
|
+
#${SHELL_ROOT_ID} .ge-pl .ge-iconbtn:hover, #${SHELL_ROOT_ID} .ge-spinwrap .ge-iconbtn:hover,
|
|
308
|
+
#${SHELL_ROOT_ID} .ge-iconbtn:hover { color:var(--shell-accent); }
|
|
309
|
+
/* turbo at rest (level 0) reads as dimmed — clearly off, but lighter than a true [disabled] control */
|
|
310
|
+
#${SHELL_ROOT_ID} [data-ge="turbo"]:not(.ge-active) { opacity:.5; }
|
|
311
|
+
#${SHELL_ROOT_ID} [data-ge="turbo"]:not(.ge-active):hover { opacity:1; }
|
|
312
|
+
/* autoplay glow while running (beats the white .ge-spinwrap/.ge-pl colour rules) */
|
|
313
|
+
#${SHELL_ROOT_ID} .ge-iconbtn.ge-glow { color:var(--shell-accent); filter:drop-shadow(0 0 6px var(--shell-accent)); }
|
|
314
|
+
/* tappable stake readout opens the bet picker */
|
|
315
|
+
#${SHELL_ROOT_ID} .ge-betbtn { pointer-events:auto; cursor:pointer; border-radius:8px; transition:color .12s ease; }
|
|
316
|
+
#${SHELL_ROOT_ID} .ge-betbtn:hover { color:var(--shell-accent); }
|
|
317
|
+
#${SHELL_ROOT_ID} .ge-betbtn.ge-disabled { pointer-events:none; }
|
|
318
|
+
/* WIN — inline between the groups, same block height/shape as the other plaques (glass tone) */
|
|
319
|
+
#${SHELL_ROOT_ID} .ge-winpill { flex:0 0 auto; align-self:center; display:flex; align-items:center;
|
|
320
|
+
justify-content:center; gap:8px; height:56px; box-sizing:border-box; padding:0 24px; border-radius:16px;
|
|
321
|
+
white-space:nowrap; pointer-events:none; background:var(--shell-plaque-glass); color:#fff;
|
|
322
|
+
font-size:16px; font-weight:800; font-variant-numeric:tabular-nums; text-shadow:none; }
|
|
323
|
+
#${SHELL_ROOT_ID} .ge-winpill .ge-lbl { display:inline; margin:0; color:rgba(255,255,255,.7);
|
|
324
|
+
font-size:10px; letter-spacing:.12em; }
|
|
325
|
+
/* lifted above the bar on overflow — compact pill (flex child of the host, scaled with it) */
|
|
326
|
+
#${SHELL_ROOT_ID} .ge-winpill.ge-up { height:auto; padding:6px 16px; border-radius:999px; }
|
|
327
|
+
|
|
328
|
+
/* centred CARD modal — opaque card on a frosted backdrop, accent title heading at the top
|
|
329
|
+
(no ✕), content, full-bleed footer button(s). Shared by the buy-bonus confirm AND the
|
|
330
|
+
bet/autoplay pickers so every centred modal is the same type. Close via backdrop click. */
|
|
331
|
+
#${SHELL_ROOT_ID} .ge-sheet { position:absolute; inset:0; z-index:60; pointer-events:auto; display:flex;
|
|
332
|
+
align-items:center; justify-content:center; padding:clamp(10px,4vh,24px); box-sizing:border-box;
|
|
333
|
+
background:rgba(12,17,28,.5); backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%);
|
|
334
|
+
-webkit-backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%); animation:ge-ov-in .16s ease-out; }
|
|
335
|
+
/* GameShell.fitModal() scales the whole card down (transform) so it fits short popouts uniformly */
|
|
336
|
+
#${SHELL_ROOT_ID} .ge-modal-card { width:100%; max-width:420px; box-sizing:border-box; overflow:hidden;
|
|
337
|
+
transform-origin:center center; background:var(--shell-plaque-solid); border-radius:20px; display:flex; flex-direction:column; }
|
|
338
|
+
/* ✕ pinned to the overlay corner (the screen), not the card */
|
|
339
|
+
#${SHELL_ROOT_ID} .ge-modal-close { position:absolute; top:12px; right:12px; z-index:2; width:36px; height:36px;
|
|
340
|
+
border:none; border-radius:50%; cursor:pointer; pointer-events:auto; background:var(--shell-plaque-dark); color:#fff;
|
|
341
|
+
display:flex; align-items:center; justify-content:center; font-size:20px; transition:background .12s ease, color .12s ease; }
|
|
342
|
+
#${SHELL_ROOT_ID} .ge-modal-close:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
|
|
343
|
+
#${SHELL_ROOT_ID} .ge-modal-body { padding:18px; display:flex; flex-direction:column; gap:16px; }
|
|
344
|
+
#${SHELL_ROOT_ID} .ge-modal-title { margin:0; text-align:center; color:var(--card-acc, var(--shell-accent));
|
|
345
|
+
font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:18px; }
|
|
346
|
+
#${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:14px; line-height:1.5; }
|
|
347
|
+
#${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap:10px; }
|
|
348
|
+
#${SHELL_ROOT_ID} .ge-chip { pointer-events:auto; cursor:pointer; border:1px solid var(--shell-plaque-line);
|
|
349
|
+
border-radius:12px; background:rgba(255,255,255,.04); color:#fff; font-size:15px; font-weight:700;
|
|
350
|
+
font-variant-numeric:tabular-nums; padding:12px 8px; transition:background .12s ease, border-color .12s ease; }
|
|
351
|
+
#${SHELL_ROOT_ID} .ge-chip:hover { background:var(--shell-plaque-glass-hover); }
|
|
352
|
+
#${SHELL_ROOT_ID} .ge-chip.ge-on { border-color:var(--shell-accent); background:var(--shell-accent); color:#fff; }
|
|
353
|
+
/* full-bleed footer button(s), flush to the card's bottom edge (card clips the corners) */
|
|
354
|
+
#${SHELL_ROOT_ID} .ge-modal-actions { display:flex; }
|
|
355
|
+
#${SHELL_ROOT_ID} .ge-modal-actions > * { flex:1; }
|
|
356
|
+
#${SHELL_ROOT_ID} .ge-modal-btn { width:100%; border:none; padding:16px; font-size:15px; font-weight:800;
|
|
357
|
+
letter-spacing:.04em; text-transform:uppercase; cursor:pointer; pointer-events:auto; transition:filter .12s ease; }
|
|
358
|
+
#${SHELL_ROOT_ID} .ge-modal-btn:hover:not([disabled]) { filter:brightness(1.08); }
|
|
359
|
+
#${SHELL_ROOT_ID} .ge-modal-btn--accent { background:var(--card-acc, var(--shell-accent)); color:#fff; }
|
|
360
|
+
#${SHELL_ROOT_ID} .ge-modal-btn--ghost { background:var(--shell-plaque-glass-hover); color:#fff; }
|
|
361
|
+
/* replay summary — label/value rows, accented total-win row */
|
|
362
|
+
#${SHELL_ROOT_ID} .ge-replay-rows { display:flex; flex-direction:column; }
|
|
363
|
+
#${SHELL_ROOT_ID} .ge-replay-row { display:flex; justify-content:space-between; align-items:baseline; gap:16px; padding:11px 2px; }
|
|
364
|
+
#${SHELL_ROOT_ID} .ge-replay-row + .ge-replay-row { border-top:1px solid var(--shell-plaque-line); }
|
|
365
|
+
#${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:11px; font-weight:700; }
|
|
366
|
+
#${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:15px; font-variant-numeric:tabular-nums; }
|
|
367
|
+
#${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:12px; }
|
|
368
|
+
#${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:19px; }
|
|
369
|
+
|
|
370
|
+
#${SHELL_ROOT_ID}.ge-shell-hidden { opacity:0; pointer-events:none; transition:opacity .25s ease; }
|
|
371
|
+
`;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ShellConfig, ShellState } from './types';
|
|
2
|
+
|
|
3
|
+
export function createInitialState(config: ShellConfig): ShellState {
|
|
4
|
+
return {
|
|
5
|
+
mode: config.mode,
|
|
6
|
+
balance: config.balance,
|
|
7
|
+
win: config.win,
|
|
8
|
+
bet: config.currentBet ?? config.defaultBet,
|
|
9
|
+
availableBets: [...config.availableBets],
|
|
10
|
+
busy: false,
|
|
11
|
+
autoplay: { active: false, remaining: 0 },
|
|
12
|
+
turbo: 0,
|
|
13
|
+
buyBonusEnabled: true,
|
|
14
|
+
freeSpins: { current: 0, total: 0, totalWin: 0, lastWin: 0 },
|
|
15
|
+
activeFeature: null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Step bet up/down within availableBets, clamped at the ends. */
|
|
20
|
+
export function stepBet(state: ShellState, direction: 1 | -1): number {
|
|
21
|
+
const idx = state.availableBets.indexOf(state.bet);
|
|
22
|
+
const next = Math.max(0, Math.min(state.availableBets.length - 1, idx + direction));
|
|
23
|
+
return state.availableBets[next];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Cycle turbo level 0..maxLevels (wraps back to 0). */
|
|
27
|
+
export function nextTurbo(current: number, maxLevels: number): number {
|
|
28
|
+
if (maxLevels <= 0) return 0;
|
|
29
|
+
return current >= maxLevels ? 0 : current + 1;
|
|
30
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ThemeConfig } from './types';
|
|
2
|
+
import { BRAND_ACCENT } from './colors';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_ACCENT = BRAND_ACCENT; // brand purple — BUY BONUS + active states (single source: colors.ts)
|
|
5
|
+
|
|
6
|
+
// Neutral palettes for each scheme. Everything the shell paints reads from these
|
|
7
|
+
// tokens, so a single `scheme` flip recolours bar, icons, overlays and toggles.
|
|
8
|
+
const PALETTE = {
|
|
9
|
+
dark: {
|
|
10
|
+
fg: '#f3f5fa', muted: '#9aa3b6', icon: '#c7cedb', iconActive: '#ffffff',
|
|
11
|
+
surface: '#0c111c', hairline: 'rgba(255,255,255,.07)',
|
|
12
|
+
veil: 'rgba(255,255,255,.05)', veilStrong: 'rgba(255,255,255,.1)', track: 'rgba(255,255,255,.16)',
|
|
13
|
+
soft: '#dfe4ee', spin: '#f4f6fb', spinFg: '#141a28',
|
|
14
|
+
},
|
|
15
|
+
light: {
|
|
16
|
+
fg: '#15202e', muted: '#5a6678', icon: '#3c4658', iconActive: '#0b1220',
|
|
17
|
+
surface: '#eef1f7', hairline: 'rgba(15,23,42,.12)',
|
|
18
|
+
veil: 'rgba(15,23,42,.05)', veilStrong: 'rgba(15,23,42,.09)', track: 'rgba(15,23,42,.22)',
|
|
19
|
+
soft: '#3a4453', spin: '#1c2434', spinFg: '#f3f6fb',
|
|
20
|
+
},
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/** CSS custom-property block for the shell root. `scheme` picks the palette;
|
|
24
|
+
* only accent and buyBonus are additionally game-overridable. */
|
|
25
|
+
export function buildThemeVars(theme: ThemeConfig = {}): string {
|
|
26
|
+
const p = PALETTE[theme.scheme === 'light' ? 'light' : 'dark'];
|
|
27
|
+
return [
|
|
28
|
+
`--shell-fg: ${p.fg}`,
|
|
29
|
+
`--shell-muted: ${p.muted}`,
|
|
30
|
+
`--shell-icon: ${p.icon}`,
|
|
31
|
+
`--shell-icon-active: ${p.iconActive}`,
|
|
32
|
+
`--shell-surface: ${p.surface}`,
|
|
33
|
+
`--shell-hairline: ${p.hairline}`,
|
|
34
|
+
`--shell-veil: ${p.veil}`,
|
|
35
|
+
`--shell-veil-strong: ${p.veilStrong}`,
|
|
36
|
+
`--shell-track: ${p.track}`,
|
|
37
|
+
`--shell-soft: ${p.soft}`,
|
|
38
|
+
`--shell-spin: ${p.spin}`,
|
|
39
|
+
`--shell-spin-fg: ${p.spinFg}`,
|
|
40
|
+
`--shell-radius: 12px`,
|
|
41
|
+
// Plaque tokens — the grouped dark/glass panel language shared by the control bar
|
|
42
|
+
// AND the overlays. Scheme-independent (always dark, white-on-dark) so bar + overlays
|
|
43
|
+
// stay visually identical regardless of the dark/light `scheme`.
|
|
44
|
+
`--shell-plaque-dark: rgba(6,9,15,.76)`,
|
|
45
|
+
`--shell-plaque-glass: rgba(30,36,48,.5)`,
|
|
46
|
+
`--shell-plaque-glass-hover: rgba(40,48,64,.62)`,
|
|
47
|
+
// Opaque surface for centred modals (confirm, bet/autoplay pickers) so they read solid,
|
|
48
|
+
// not see-through, over the frosted backdrop.
|
|
49
|
+
`--shell-plaque-solid: #1a2030`,
|
|
50
|
+
`--shell-plaque-line: rgba(255,255,255,.22)`,
|
|
51
|
+
`--shell-plaque-label: rgba(255,255,255,.6)`,
|
|
52
|
+
`--shell-accent: ${theme.accent ?? DEFAULT_ACCENT}`,
|
|
53
|
+
// buy-bonus tint follows the accent by default; only overridden when set explicitly
|
|
54
|
+
`--shell-buybonus: ${theme.buyBonusColor ?? theme.accent ?? DEFAULT_ACCENT}`,
|
|
55
|
+
].join('; ') + ';';
|
|
56
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export type ShellMode = 'base' | 'freeSpins' | 'replay';
|
|
2
|
+
|
|
3
|
+
export interface CurrencyConfig {
|
|
4
|
+
symbol: string;
|
|
5
|
+
position: 'left' | 'right';
|
|
6
|
+
/** Maximum fraction digits (default 2). The value is rounded to this precision. */
|
|
7
|
+
decimals?: number;
|
|
8
|
+
/** Minimum fraction digits (defaults to `decimals`). Trailing zeros are trimmed down to
|
|
9
|
+
* this many places, so small wins keep their significant digits (e.g. 0.0673) while round
|
|
10
|
+
* amounts stay compact (e.g. 0.30). */
|
|
11
|
+
minDecimals?: number;
|
|
12
|
+
separator?: { thousands?: string; decimal?: string };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BonusOption {
|
|
16
|
+
id: string;
|
|
17
|
+
/** 'bonus' buys into a bonus round, 'feature' toggles a base-game modifier (e.g. Ante).
|
|
18
|
+
* Drives the card/button label and accent. Defaults to 'bonus'. */
|
|
19
|
+
type?: 'feature' | 'bonus';
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
/** Transparent art image shown at the top of the card (no background plate). */
|
|
23
|
+
thumbnail?: string;
|
|
24
|
+
volatility?: 1 | 2 | 3 | 4 | 5;
|
|
25
|
+
/** Card price = priceMultiplier × current bet, rendered in the shell currency. */
|
|
26
|
+
priceMultiplier: number;
|
|
27
|
+
/** Per-option accent override. Falls back to the type default (bonus → purple, feature → gold). */
|
|
28
|
+
accentColor?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ThemeConfig {
|
|
32
|
+
/** Palette scheme: 'dark' (default) for dark games, 'light' for light backgrounds. */
|
|
33
|
+
scheme?: 'dark' | 'light';
|
|
34
|
+
accent?: string;
|
|
35
|
+
buyBonusColor?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One paytable entry: a symbol (text/image) and its win tiers, rendered "<count> x<multiplier>". */
|
|
39
|
+
export interface PaytableRow {
|
|
40
|
+
symbol: { text?: string; image?: string };
|
|
41
|
+
wins: Array<{ count?: string; multiplier: number }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One payline over a cols×rows grid: the row index (0 = top) the line takes in each column. */
|
|
45
|
+
export interface PaylineDef {
|
|
46
|
+
/** length must equal grid.cols; each value in 0..rows-1 */
|
|
47
|
+
pattern: number[];
|
|
48
|
+
label?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A single grid cell, 0-based, row 0 = top. */
|
|
52
|
+
export type CellRef = [col: number, row: number];
|
|
53
|
+
|
|
54
|
+
/** How a game pays — drives the GameInfo win-section illustration. One section = one kind.
|
|
55
|
+
* `example`/`winExample`/`loseExample` are optional; omit them for an auto-drawn illustration
|
|
56
|
+
* sized to `grid`. */
|
|
57
|
+
export type WinSection = {
|
|
58
|
+
type: 'wins';
|
|
59
|
+
title?: string;
|
|
60
|
+
order?: number;
|
|
61
|
+
grid: { cols: number; rows: number };
|
|
62
|
+
/** Optional prose shown alongside the illustration. */
|
|
63
|
+
description?: string;
|
|
64
|
+
} & (
|
|
65
|
+
| { kind: 'classic'; lines: Array<number[] | PaylineDef> }
|
|
66
|
+
| { kind: 'cluster'; minCount: number; example?: CellRef[] }
|
|
67
|
+
| { kind: 'anywhere'; minCount: number; example?: CellRef[] }
|
|
68
|
+
| { kind: 'ways'; winExample?: CellRef[]; loseExample?: CellRef[] }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/** A playable mode / bonus-buy option, shown for comparison (informational only). */
|
|
72
|
+
export interface GameMode {
|
|
73
|
+
title: string;
|
|
74
|
+
price?: string;
|
|
75
|
+
rtp?: number;
|
|
76
|
+
maxWin?: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A preset game-info section. `order` overrides placement; by default `modes` comes
|
|
81
|
+
* first, `controls` second, and the rest follow in declaration order. */
|
|
82
|
+
export type GameInfoSection =
|
|
83
|
+
| { type: 'modes'; title?: string; order?: number; modes: GameMode[] }
|
|
84
|
+
| { type: 'controls'; title?: string; order?: number }
|
|
85
|
+
| { type: 'paytable'; title?: string; order?: number; rows: PaytableRow[] }
|
|
86
|
+
| WinSection
|
|
87
|
+
| { type: 'custom'; title?: string; order?: number; node?: HTMLElement; html?: string };
|
|
88
|
+
|
|
89
|
+
export interface GameInfoContent {
|
|
90
|
+
sections?: GameInfoSection[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ShellFeatures {
|
|
94
|
+
turbo: 0 | 1 | 2 | 3;
|
|
95
|
+
autoplay: boolean;
|
|
96
|
+
buyBonus: BonusOption[] | false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AutoplayOptions {
|
|
100
|
+
active: boolean;
|
|
101
|
+
remaining: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface FreeSpinsState {
|
|
105
|
+
current: number;
|
|
106
|
+
total: number;
|
|
107
|
+
totalWin: number;
|
|
108
|
+
lastWin: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One footer button of a generic modal. Clicking it runs `on` (if any), then closes the modal. */
|
|
112
|
+
export interface ModalAction {
|
|
113
|
+
title: string;
|
|
114
|
+
/** Button fill colour (any CSS colour). Omit for a neutral/secondary button. */
|
|
115
|
+
color?: string;
|
|
116
|
+
on?: () => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Options for `shell.openReplay()` — a non-dismissable replay summary modal.
|
|
120
|
+
* `bonusId` is matched against `features.buyBonus` to label the mode and read the cost
|
|
121
|
+
* multiplier. There is no ✕ and the backdrop never closes it; the only action is START
|
|
122
|
+
* REPLAY, which closes the modal, runs `onReplay`, then reopens it. */
|
|
123
|
+
export interface ReplayModalOptions {
|
|
124
|
+
bonusId: string;
|
|
125
|
+
/** Base bet the replay was recorded at. */
|
|
126
|
+
bet: number;
|
|
127
|
+
payoutMultiplier: number;
|
|
128
|
+
/** Runs after the modal closes; the modal reopens once it resolves (immediately for sync). */
|
|
129
|
+
onReplay: () => void | Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Options for `shell.openModal()` — a generic, externally-triggered card modal. */
|
|
133
|
+
export interface ModalOptions {
|
|
134
|
+
/** Show the ✕ in the overlay's top-right corner. */
|
|
135
|
+
availableClose: boolean;
|
|
136
|
+
title: string;
|
|
137
|
+
body: string;
|
|
138
|
+
/** Footer buttons; each closes the modal (after running its `on`). */
|
|
139
|
+
actions?: ModalAction[];
|
|
140
|
+
/** Backdrop blur in px (defaults to the shell's standard blur). */
|
|
141
|
+
blurLevel?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ShellConfig {
|
|
145
|
+
mount: HTMLElement;
|
|
146
|
+
theme?: ThemeConfig;
|
|
147
|
+
gameInfo: GameInfoContent;
|
|
148
|
+
language: string;
|
|
149
|
+
/** When true, all built-in shell text is shown in the social-casino vocabulary (derived from
|
|
150
|
+
* English via word-swap rules), regardless of `language`. Game-supplied content is untouched. */
|
|
151
|
+
isSocial?: boolean;
|
|
152
|
+
currency: CurrencyConfig;
|
|
153
|
+
availableBets: number[];
|
|
154
|
+
defaultBet: number;
|
|
155
|
+
currentBet: number | null;
|
|
156
|
+
balance: number;
|
|
157
|
+
win: number;
|
|
158
|
+
mode: ShellMode;
|
|
159
|
+
features: ShellFeatures;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ShellState {
|
|
163
|
+
mode: ShellMode;
|
|
164
|
+
balance: number;
|
|
165
|
+
win: number;
|
|
166
|
+
bet: number;
|
|
167
|
+
availableBets: number[];
|
|
168
|
+
busy: boolean;
|
|
169
|
+
autoplay: AutoplayOptions;
|
|
170
|
+
turbo: number;
|
|
171
|
+
buyBonusEnabled: boolean;
|
|
172
|
+
freeSpins: FreeSpinsState;
|
|
173
|
+
/** The currently activated `feature` option (e.g. Ante), or null. Drives the
|
|
174
|
+
* effective-bet readout tint and the BUY BONUS → DISABLE toggle on the bar. */
|
|
175
|
+
activeFeature: BonusOption | null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ShellEvents {
|
|
179
|
+
spin: void;
|
|
180
|
+
betChange: number;
|
|
181
|
+
autoplayStart: AutoplayOptions;
|
|
182
|
+
autoplayStop: void;
|
|
183
|
+
turboChange: number;
|
|
184
|
+
buyBonusSelect: { id: string };
|
|
185
|
+
featureActivate: { id: string };
|
|
186
|
+
featureDeactivate: { id: string };
|
|
187
|
+
menuOpen: void;
|
|
188
|
+
settingsOpen: void;
|
|
189
|
+
infoOpen: void;
|
|
190
|
+
settingChange: { key: string; value: unknown };
|
|
191
|
+
}
|