@energy8platform/platform-core 0.25.3 → 0.26.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/game-spec.d.ts +3 -0
- package/dist/index.cjs.js +1785 -212
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +83 -12
- package/dist/index.esm.js +1785 -212
- package/dist/index.esm.js.map +1 -1
- package/dist/loading.cjs.js +237 -90
- package/dist/loading.cjs.js.map +1 -1
- package/dist/loading.d.ts +52 -2
- package/dist/loading.esm.js +235 -90
- package/dist/loading.esm.js.map +1 -1
- package/dist/shell.cjs.js +1552 -122
- package/dist/shell.cjs.js.map +1 -1
- package/dist/shell.d.ts +49 -14
- package/dist/shell.esm.js +1551 -123
- package/dist/shell.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/game-spec/types.ts +3 -0
- package/src/loading/CSSPreloader.ts +21 -115
- package/src/loading/index.ts +6 -0
- package/src/loading/variants/energy8.ts +105 -0
- package/src/loading/variants/index.ts +19 -0
- package/src/loading/variants/types.ts +36 -0
- package/src/loading/variants/voidmoon.ts +134 -0
- package/src/shell/GameShell.ts +118 -74
- package/src/shell/components/BuyBonus.ts +157 -14
- package/src/shell/components/GameInfo.ts +104 -5
- package/src/shell/components/Settings.ts +9 -10
- package/src/shell/components/pickers.ts +66 -10
- package/src/shell/components/primitives.ts +4 -3
- package/src/shell/i18n.ts +23 -0
- package/src/shell/index.ts +2 -1
- package/src/shell/keyboard.ts +229 -0
- package/src/shell/locales.ts +864 -0
- package/src/shell/shell.css.ts +20 -3
- package/src/shell/types.ts +8 -0
- package/src/shell/version.ts +1 -1
- package/src/types.ts +8 -0
package/dist/index.cjs.js
CHANGED
|
@@ -712,7 +712,7 @@ const GRADIENT_DEFS = `
|
|
|
712
712
|
<stop stop-color="#316FB0"/><stop stop-color="#1FCDE6" offset=".5"/><stop stop-color="#29FEE7" offset="1"/>
|
|
713
713
|
</linearGradient>`;
|
|
714
714
|
/** Max width of the loader bar in SVG units */
|
|
715
|
-
const LOADER_BAR_MAX_WIDTH = 174;
|
|
715
|
+
const LOADER_BAR_MAX_WIDTH$1 = 174;
|
|
716
716
|
/**
|
|
717
717
|
* Build the Energy8 SVG logo with a loader bar, using unique IDs.
|
|
718
718
|
*
|
|
@@ -745,61 +745,27 @@ ${defs}
|
|
|
745
745
|
</svg>`;
|
|
746
746
|
}
|
|
747
747
|
|
|
748
|
-
|
|
749
|
-
const RECT_ID = 'ge-pl-loader-rect';
|
|
750
|
-
const TEXT_ID = 'ge-pl-loader-text';
|
|
751
|
-
const
|
|
752
|
-
const LOGO_SVG = buildLogoSVG({
|
|
748
|
+
/** Element ids the lifecycle handle binds to (also asserted by tests). */
|
|
749
|
+
const RECT_ID$1 = 'ge-pl-loader-rect';
|
|
750
|
+
const TEXT_ID$1 = 'ge-pl-loader-text';
|
|
751
|
+
const LOGO_SVG$1 = buildLogoSVG({
|
|
753
752
|
idPrefix: 'pl',
|
|
754
753
|
svgClass: 'ge-logo-svg',
|
|
755
754
|
clipRectClass: 'ge-clip-rect',
|
|
756
|
-
clipRectId: RECT_ID,
|
|
755
|
+
clipRectId: RECT_ID$1,
|
|
757
756
|
textClass: 'ge-preloader-svg-text',
|
|
758
|
-
textId: TEXT_ID,
|
|
757
|
+
textId: TEXT_ID$1,
|
|
759
758
|
});
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
return
|
|
764
|
-
return Math.max(0, Math.min(1, p));
|
|
765
|
-
}
|
|
766
|
-
function createCSSPreloader(container, config) {
|
|
767
|
-
if (document.getElementById(PRELOADER_ID))
|
|
768
|
-
return;
|
|
769
|
-
const bgColor = typeof config?.backgroundColor === 'string'
|
|
770
|
-
? config.backgroundColor
|
|
771
|
-
: typeof config?.backgroundColor === 'number'
|
|
772
|
-
? `#${config.backgroundColor.toString(16).padStart(6, '0')}`
|
|
773
|
-
: '#0a0a1a';
|
|
774
|
-
const bgGradient = config?.backgroundGradient ?? `linear-gradient(135deg, ${bgColor} 0%, #1a1a3e 100%)`;
|
|
775
|
-
const customHTML = config?.cssPreloaderHTML ?? '';
|
|
776
|
-
const overlay = document.createElement('div');
|
|
777
|
-
overlay.id = PRELOADER_ID;
|
|
778
|
-
overlay.innerHTML = customHTML || `
|
|
759
|
+
/** The default Energy8-branded preloader: animated wordmark + shimmering loader bar. */
|
|
760
|
+
const energy8Variant = {
|
|
761
|
+
buildContentHTML() {
|
|
762
|
+
return `
|
|
779
763
|
<div class="ge-preloader-content">
|
|
780
|
-
${LOGO_SVG}
|
|
764
|
+
${LOGO_SVG$1}
|
|
781
765
|
</div>
|
|
782
766
|
`;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
#${PRELOADER_ID} {
|
|
786
|
-
position: absolute;
|
|
787
|
-
top: 0; left: 0;
|
|
788
|
-
width: 100%; height: 100%;
|
|
789
|
-
background: ${bgGradient};
|
|
790
|
-
display: flex;
|
|
791
|
-
align-items: center;
|
|
792
|
-
justify-content: center;
|
|
793
|
-
z-index: 10000;
|
|
794
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
795
|
-
transition: opacity 0.4s ease-out;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
#${PRELOADER_ID}.ge-preloader-hidden {
|
|
799
|
-
opacity: 0;
|
|
800
|
-
pointer-events: none;
|
|
801
|
-
}
|
|
802
|
-
|
|
767
|
+
},
|
|
768
|
+
css: `
|
|
803
769
|
.ge-preloader-content {
|
|
804
770
|
display: flex;
|
|
805
771
|
flex-direction: column;
|
|
@@ -850,6 +816,219 @@ function createCSSPreloader(container, config) {
|
|
|
850
816
|
0%, 100% { opacity: 0.5; }
|
|
851
817
|
50% { opacity: 1; }
|
|
852
818
|
}
|
|
819
|
+
`,
|
|
820
|
+
mount(overlay) {
|
|
821
|
+
const rectEl = overlay.querySelector(`#${RECT_ID$1}`);
|
|
822
|
+
const textEl = overlay.querySelector(`#${TEXT_ID$1}`);
|
|
823
|
+
// Custom HTML mode (or missing logo) — no progress target; lifecycle inert.
|
|
824
|
+
if (!rectEl || !textEl)
|
|
825
|
+
return null;
|
|
826
|
+
let driven = false;
|
|
827
|
+
return {
|
|
828
|
+
setProgress(p, showPercentage) {
|
|
829
|
+
if (!driven) {
|
|
830
|
+
rectEl.classList.add('driven');
|
|
831
|
+
driven = true;
|
|
832
|
+
}
|
|
833
|
+
rectEl.setAttribute('width', String(p * LOADER_BAR_MAX_WIDTH$1));
|
|
834
|
+
if (showPercentage) {
|
|
835
|
+
textEl.textContent = `${Math.round(p * 100)}%`;
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
showTapText(text) {
|
|
839
|
+
textEl.textContent = text;
|
|
840
|
+
textEl.classList.add('ge-svg-pulse');
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
/** Element ids the lifecycle handle binds to. */
|
|
847
|
+
const RECT_ID = 'ge-vm-loader-rect';
|
|
848
|
+
const TEXT_ID = 'ge-vm-loader-text';
|
|
849
|
+
/** Max width (SVG units) of the voidmoon loader bar fill. Spans the first 'o' → end of the crescent. */
|
|
850
|
+
const LOADER_BAR_MAX_WIDTH = 751;
|
|
851
|
+
/**
|
|
852
|
+
* "voidmoon" wordmark — the official logo, embedded verbatim as SVG outlines:
|
|
853
|
+
* thin white letters with the final "o" of "moon" rendered as a purple crescent
|
|
854
|
+
* (#9D63FE). The glyphs live in a flipped group (`translate(0,941) scale(1,-1)`)
|
|
855
|
+
* exactly as exported; the loader bar + status text are added beneath it in the
|
|
856
|
+
* outer (un-flipped) viewBox space.
|
|
857
|
+
*/
|
|
858
|
+
const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="301 339 1075 335" class="ge-vm-logo-svg" style="overflow:visible" role="img">
|
|
859
|
+
<title>voidmoon</title>
|
|
860
|
+
<g transform="translate(0,941) scale(1,-1)">
|
|
861
|
+
<g fill="#ffffff" fill-rule="evenodd">
|
|
862
|
+
<path d="M627 562 c-4 -2 -7 -6 -7 -12 0 -13 14 -18 23 -10 3 3 3 5 4 9 0 7 -2 11 -7 13 -5 2 -9 2 -13 0z"/>
|
|
863
|
+
<path d="M780 531 l0 -31 -6 5 c-14 14 -35 17 -56 10 -24 -8 -40 -28 -42 -53 0 -11 1 -19 6 -30 8 -16 22 -27 40 -32 8 -2 23 -2 31 0 18 4 34 17 42 33 2 5 4 11 5 14 1 3 1 24 1 61 l0 55 -10 0 -11 0 0 -32z m-24 -38 c20 -10 28 -31 20 -50 -3 -6 -13 -16 -19 -19 -21 -10 -45 -2 -56 18 -2 4 -2 7 -3 14 -1 14 4 24 15 33 8 6 15 8 27 8 9 -1 10 -1 16 -4z"/>
|
|
864
|
+
<path d="M520 518 c-26 -6 -45 -25 -50 -51 -1 -9 -1 -11 0 -19 3 -12 7 -21 15 -30 8 -8 16 -13 28 -17 6 -2 9 -2 19 -2 10 0 13 0 20 2 21 8 36 24 41 46 1 8 1 13 0 22 -5 23 -21 40 -44 47 -6 2 -23 3 -29 2z m29 -25 c9 -4 16 -11 20 -19 2 -5 3 -6 3 -15 0 -10 -1 -11 -3 -16 -8 -15 -23 -24 -40 -23 -9 1 -15 3 -22 8 -22 15 -21 48 2 63 7 4 14 6 24 6 8 -1 10 -1 16 -4z"/>
|
|
865
|
+
<path d="M869 518 c-21 -4 -35 -18 -40 -38 -1 -6 -1 -14 -1 -43 l1 -35 10 0 11 0 0 37 c0 33 1 38 2 41 3 7 7 11 13 14 5 2 8 3 13 3 11 0 20 -5 25 -16 l3 -5 0 -37 1 -37 10 0 10 0 0 37 c0 35 1 37 3 42 5 10 14 16 26 16 11 0 21 -7 25 -17 2 -5 2 -7 2 -41 0 -19 0 -36 1 -37 0 -1 3 -1 11 -1 l10 1 0 35 c0 23 0 38 -1 42 -2 9 -8 21 -14 27 -20 17 -51 17 -68 -1 l-5 -5 -5 5 c-9 9 -19 13 -31 14 -5 0 -10 0 -12 -1z"/>
|
|
866
|
+
<path d="M1077 517 c-9 -1 -20 -7 -27 -12 -7 -6 -14 -16 -18 -25 -4 -11 -5 -25 -3 -35 5 -21 21 -37 42 -44 38 -12 78 14 81 53 1 15 -4 31 -14 43 -14 17 -38 25 -61 20z m25 -22 c19 -5 31 -24 28 -42 -4 -26 -34 -41 -59 -29 -20 10 -27 32 -17 52 8 16 29 25 48 19z"/>
|
|
867
|
+
<path d="M1282 516 c-18 -5 -34 -21 -38 -39 -1 -4 -1 -18 -1 -40 l1 -35 10 -1 10 0 0 32 c0 20 0 35 1 38 2 11 8 18 18 23 7 3 18 3 26 0 6 -3 12 -9 15 -16 2 -4 3 -6 3 -40 l1 -36 10 0 11 0 0 33 c0 38 0 43 -6 54 -7 14 -19 24 -34 28 -7 1 -20 1 -27 -1z"/>
|
|
868
|
+
<path d="M329 515 c0 -1 2 -5 4 -9 2 -5 13 -28 24 -53 12 -24 21 -45 22 -46 2 -4 10 -7 15 -7 4 0 11 3 13 6 3 2 50 105 50 108 0 1 -3 1 -11 1 l-11 0 -7 -14 c-3 -8 -12 -28 -20 -45 -7 -17 -13 -31 -14 -31 -1 -1 -3 5 -17 35 -19 43 -23 53 -24 54 -1 1 -5 1 -13 1 -6 0 -11 0 -11 0z"/>
|
|
869
|
+
<path d="M623 514 c0 -1 0 -26 0 -57 l1 -55 10 0 10 0 0 56 0 57 -10 0 c-7 0 -10 0 -11 -1z"/>
|
|
870
|
+
</g>
|
|
871
|
+
<g fill="#9D63FE" fill-rule="evenodd">
|
|
872
|
+
<path d="M1150 515 c-3 0 -6 -1 -6 -1 0 -1 2 -2 5 -2 10 -4 26 -17 31 -27 11 -21 8 -44 -7 -62 -5 -7 -17 -16 -24 -18 -3 -1 -5 -2 -4 -2 0 -2 16 -3 24 -3 35 3 59 40 49 74 -2 8 -7 18 -13 24 -5 6 -15 13 -23 16 -8 3 -24 4 -32 1z"/>
|
|
873
|
+
</g>
|
|
874
|
+
</g>
|
|
875
|
+
|
|
876
|
+
<rect x="469" y="600" width="751" height="9" rx="4.5" fill="rgba(255,255,255,0.12)"/>
|
|
877
|
+
<clipPath id="vm-loader-clip">
|
|
878
|
+
<rect id="${RECT_ID}" x="469" y="600" width="0" height="9" rx="4.5" class="ge-vm-clip-rect"/>
|
|
879
|
+
</clipPath>
|
|
880
|
+
<rect x="469" y="600" width="751" height="9" rx="4.5" fill="#9D63FE" clip-path="url(#vm-loader-clip)"/>
|
|
881
|
+
|
|
882
|
+
<text id="${TEXT_ID}" x="844.5" y="650" text-anchor="middle" class="ge-vm-text">Loading...</text>
|
|
883
|
+
</svg>`;
|
|
884
|
+
const voidmoonVariant = {
|
|
885
|
+
buildContentHTML() {
|
|
886
|
+
return `
|
|
887
|
+
<div class="ge-vm-content">
|
|
888
|
+
${LOGO_SVG}
|
|
889
|
+
</div>
|
|
890
|
+
`;
|
|
891
|
+
},
|
|
892
|
+
css: `
|
|
893
|
+
.ge-vm-content {
|
|
894
|
+
display: flex;
|
|
895
|
+
flex-direction: column;
|
|
896
|
+
align-items: center;
|
|
897
|
+
width: 82%;
|
|
898
|
+
max-width: 680px;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.ge-vm-logo-svg {
|
|
902
|
+
width: 100%;
|
|
903
|
+
height: auto;
|
|
904
|
+
filter: drop-shadow(0 0 26px rgba(157, 99, 254, 0.3));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* Shimmer the loader bar while waiting */
|
|
908
|
+
.ge-vm-clip-rect {
|
|
909
|
+
animation: ge-vm-fill 2s ease-in-out infinite;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
@keyframes ge-vm-fill {
|
|
913
|
+
0% { width: 0; }
|
|
914
|
+
50% { width: 751; }
|
|
915
|
+
100% { width: 0; }
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* Stop shimmer once JS-driven progress takes over. */
|
|
919
|
+
.ge-vm-clip-rect.driven {
|
|
920
|
+
animation: none;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.ge-vm-text {
|
|
924
|
+
fill: rgba(255, 255, 255, 0.6);
|
|
925
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
926
|
+
font-size: 20px;
|
|
927
|
+
font-weight: 600;
|
|
928
|
+
letter-spacing: 3px;
|
|
929
|
+
animation: ge-vm-pulse 1.5s ease-in-out infinite;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
@keyframes ge-vm-pulse {
|
|
933
|
+
0%, 100% { opacity: 0.4; }
|
|
934
|
+
50% { opacity: 1; }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* Tap-to-start CTA pulse. Compound selector outweighs the ambient
|
|
938
|
+
.ge-vm-text rule, swapping the animation cleanly. */
|
|
939
|
+
.ge-vm-text.ge-vm-tap-pulse {
|
|
940
|
+
animation: ge-vm-tap 1.2s ease-in-out infinite;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
@keyframes ge-vm-tap {
|
|
944
|
+
0%, 100% { opacity: 0.5; }
|
|
945
|
+
50% { opacity: 1; }
|
|
946
|
+
}
|
|
947
|
+
`,
|
|
948
|
+
mount(overlay) {
|
|
949
|
+
const rectEl = overlay.querySelector(`#${RECT_ID}`);
|
|
950
|
+
const textEl = overlay.querySelector(`#${TEXT_ID}`);
|
|
951
|
+
if (!rectEl || !textEl)
|
|
952
|
+
return null;
|
|
953
|
+
let driven = false;
|
|
954
|
+
return {
|
|
955
|
+
setProgress(p, showPercentage) {
|
|
956
|
+
if (!driven) {
|
|
957
|
+
rectEl.classList.add('driven');
|
|
958
|
+
driven = true;
|
|
959
|
+
}
|
|
960
|
+
rectEl.setAttribute('width', String(p * LOADER_BAR_MAX_WIDTH));
|
|
961
|
+
if (showPercentage) {
|
|
962
|
+
textEl.textContent = `${Math.round(p * 100)}%`;
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
showTapText(text) {
|
|
966
|
+
textEl.textContent = text;
|
|
967
|
+
textEl.classList.add('ge-vm-tap-pulse');
|
|
968
|
+
},
|
|
969
|
+
};
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Registry of selectable preloader variants. Add a new variant by writing a
|
|
975
|
+
* file in this folder and adding one entry here — `PreloaderVariantName` and
|
|
976
|
+
* `LoadingScreenConfig.preloaderVariant` widen automatically.
|
|
977
|
+
*/
|
|
978
|
+
const VARIANTS = {
|
|
979
|
+
energy8: energy8Variant,
|
|
980
|
+
voidmoon: voidmoonVariant,
|
|
981
|
+
};
|
|
982
|
+
/** Default variant used when `preloaderVariant` is omitted or unknown. */
|
|
983
|
+
const DEFAULT_VARIANT_NAME = 'energy8';
|
|
984
|
+
|
|
985
|
+
const PRELOADER_ID = '__ge-css-preloader__';
|
|
986
|
+
const REMOVE_FADE_TIMEOUT_MS = 600;
|
|
987
|
+
let state = null;
|
|
988
|
+
function clampProgress(p) {
|
|
989
|
+
if (!Number.isFinite(p))
|
|
990
|
+
return 0;
|
|
991
|
+
return Math.max(0, Math.min(1, p));
|
|
992
|
+
}
|
|
993
|
+
function createCSSPreloader(container, config) {
|
|
994
|
+
if (document.getElementById(PRELOADER_ID))
|
|
995
|
+
return;
|
|
996
|
+
const bgColor = typeof config?.backgroundColor === 'string'
|
|
997
|
+
? config.backgroundColor
|
|
998
|
+
: typeof config?.backgroundColor === 'number'
|
|
999
|
+
? `#${config.backgroundColor.toString(16).padStart(6, '0')}`
|
|
1000
|
+
: '#0a0a1a';
|
|
1001
|
+
const bgGradient = config?.backgroundGradient ?? `linear-gradient(135deg, ${bgColor} 0%, #1a1a3e 100%)`;
|
|
1002
|
+
const customHTML = config?.cssPreloaderHTML ?? '';
|
|
1003
|
+
// Pick the visual identity. Unknown names fall back to the default so a bad
|
|
1004
|
+
// config value degrades to a working preloader rather than a blank overlay.
|
|
1005
|
+
const variant = VARIANTS[config?.preloaderVariant ?? DEFAULT_VARIANT_NAME] ??
|
|
1006
|
+
VARIANTS[DEFAULT_VARIANT_NAME];
|
|
1007
|
+
const overlay = document.createElement('div');
|
|
1008
|
+
overlay.id = PRELOADER_ID;
|
|
1009
|
+
overlay.innerHTML = customHTML || variant.buildContentHTML(config);
|
|
1010
|
+
const styleEl = document.createElement('style');
|
|
1011
|
+
// Shared overlay infrastructure (positioning / background / fade) plus the
|
|
1012
|
+
// variant's own content styling and animations.
|
|
1013
|
+
styleEl.textContent = `
|
|
1014
|
+
#${PRELOADER_ID} {
|
|
1015
|
+
position: absolute;
|
|
1016
|
+
top: 0; left: 0;
|
|
1017
|
+
width: 100%; height: 100%;
|
|
1018
|
+
background: ${bgGradient};
|
|
1019
|
+
display: flex;
|
|
1020
|
+
align-items: center;
|
|
1021
|
+
justify-content: center;
|
|
1022
|
+
z-index: 10000;
|
|
1023
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1024
|
+
transition: opacity 0.4s ease-out;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#${PRELOADER_ID}.ge-preloader-hidden {
|
|
1028
|
+
opacity: 0;
|
|
1029
|
+
pointer-events: none;
|
|
1030
|
+
}
|
|
1031
|
+
${variant.css}
|
|
853
1032
|
`;
|
|
854
1033
|
// The absolute overlay needs a positioned ancestor. Only override a STATIC container, and
|
|
855
1034
|
// remember the prior inline value so removeCSSPreloader can restore it (an inline `relative`
|
|
@@ -858,41 +1037,18 @@ function createCSSPreloader(container, config) {
|
|
|
858
1037
|
container.style.position = container.style.position || 'relative';
|
|
859
1038
|
container.appendChild(styleEl);
|
|
860
1039
|
container.appendChild(overlay);
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// Custom HTML mode — no logo SVG, lifecycle API becomes mostly inert.
|
|
865
|
-
// We still record state so removeCSSPreloader works.
|
|
866
|
-
state = {
|
|
867
|
-
container,
|
|
868
|
-
prevPosition,
|
|
869
|
-
overlay,
|
|
870
|
-
styleEl,
|
|
871
|
-
rectEl: null,
|
|
872
|
-
textEl: null,
|
|
873
|
-
showPercentage: false,
|
|
874
|
-
tapToStart: config?.tapToStart !== false,
|
|
875
|
-
tapToStartText: config?.tapToStartText ?? 'TAP TO START',
|
|
876
|
-
driven: false,
|
|
877
|
-
tapState: 'idle',
|
|
878
|
-
tapPromise: null,
|
|
879
|
-
tapResolve: null,
|
|
880
|
-
tapHandler: null,
|
|
881
|
-
removed: false,
|
|
882
|
-
};
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
1040
|
+
// Custom HTML bypasses the variant's content, so there is no progress target
|
|
1041
|
+
// to bind to and the handle stays null (lifecycle API becomes inert).
|
|
1042
|
+
const handle = customHTML ? null : variant.mount(overlay, config);
|
|
885
1043
|
state = {
|
|
886
1044
|
container,
|
|
887
1045
|
prevPosition,
|
|
888
1046
|
overlay,
|
|
889
1047
|
styleEl,
|
|
890
|
-
|
|
891
|
-
textEl,
|
|
1048
|
+
handle,
|
|
892
1049
|
showPercentage: config?.showPercentage === true,
|
|
893
1050
|
tapToStart: config?.tapToStart !== false,
|
|
894
1051
|
tapToStartText: config?.tapToStartText ?? 'TAP TO START',
|
|
895
|
-
driven: false,
|
|
896
1052
|
tapState: 'idle',
|
|
897
1053
|
tapPromise: null,
|
|
898
1054
|
tapResolve: null,
|
|
@@ -905,17 +1061,9 @@ function setCSSPreloaderProgress(progress) {
|
|
|
905
1061
|
return;
|
|
906
1062
|
if (state.tapState === 'waiting' || state.tapState === 'resolved')
|
|
907
1063
|
return;
|
|
908
|
-
if (!state.
|
|
1064
|
+
if (!state.handle)
|
|
909
1065
|
return;
|
|
910
|
-
|
|
911
|
-
if (!state.driven) {
|
|
912
|
-
state.rectEl.classList.add('driven');
|
|
913
|
-
state.driven = true;
|
|
914
|
-
}
|
|
915
|
-
state.rectEl.setAttribute('width', String(p * LOADER_BAR_MAX_WIDTH));
|
|
916
|
-
if (state.showPercentage && state.textEl) {
|
|
917
|
-
state.textEl.textContent = `${Math.round(p * 100)}%`;
|
|
918
|
-
}
|
|
1066
|
+
state.handle.setProgress(clampProgress(progress), state.showPercentage);
|
|
919
1067
|
}
|
|
920
1068
|
function waitCSSPreloaderTap() {
|
|
921
1069
|
if (!state) {
|
|
@@ -927,10 +1075,7 @@ function waitCSSPreloaderTap() {
|
|
|
927
1075
|
return Promise.resolve();
|
|
928
1076
|
if (state.tapPromise)
|
|
929
1077
|
return state.tapPromise;
|
|
930
|
-
|
|
931
|
-
state.textEl.textContent = state.tapToStartText;
|
|
932
|
-
state.textEl.classList.add('ge-svg-pulse');
|
|
933
|
-
}
|
|
1078
|
+
state.handle?.showTapText(state.tapToStartText);
|
|
934
1079
|
state.overlay.style.cursor = 'pointer';
|
|
935
1080
|
state.tapState = 'waiting';
|
|
936
1081
|
state.tapPromise = new Promise((resolve) => {
|
|
@@ -984,6 +1129,221 @@ function removeCSSPreloader(_container) {
|
|
|
984
1129
|
});
|
|
985
1130
|
}
|
|
986
1131
|
|
|
1132
|
+
// Bet key detection: bet-up needs Shift for arrow/equal, NumpadAdd is bare; same logic for down.
|
|
1133
|
+
// Exported so overlays with their own bet stepper (Buy bonus) honour the SAME keys as the bar.
|
|
1134
|
+
function betDir(e) {
|
|
1135
|
+
if (e.code === 'ArrowUp' && e.shiftKey)
|
|
1136
|
+
return 1;
|
|
1137
|
+
if (e.code === 'Equal' && e.shiftKey)
|
|
1138
|
+
return 1;
|
|
1139
|
+
if (e.code === 'NumpadAdd')
|
|
1140
|
+
return 1;
|
|
1141
|
+
if (e.code === 'ArrowDown' && e.shiftKey)
|
|
1142
|
+
return -1;
|
|
1143
|
+
if (e.code === 'Minus' && e.shiftKey)
|
|
1144
|
+
return -1;
|
|
1145
|
+
if (e.code === 'NumpadSubtract')
|
|
1146
|
+
return -1;
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
class KeyboardController {
|
|
1150
|
+
host;
|
|
1151
|
+
doc;
|
|
1152
|
+
spaceHeld = false;
|
|
1153
|
+
holdTimer = null;
|
|
1154
|
+
// Bet hold-repeat state
|
|
1155
|
+
betHeldCode = null;
|
|
1156
|
+
betTimer = null;
|
|
1157
|
+
constructor(host, doc) {
|
|
1158
|
+
this.host = host;
|
|
1159
|
+
this.doc = doc ?? (typeof document !== 'undefined' ? document : null);
|
|
1160
|
+
}
|
|
1161
|
+
isSpinAllowed() {
|
|
1162
|
+
const h = this.host;
|
|
1163
|
+
const s = h.state;
|
|
1164
|
+
return (h.spacebarEnabled &&
|
|
1165
|
+
h.hotkeysEnabled &&
|
|
1166
|
+
!h.hasOpenLayer() &&
|
|
1167
|
+
s.mode === 'base' &&
|
|
1168
|
+
!s.autoplay.active);
|
|
1169
|
+
}
|
|
1170
|
+
isBetAllowed() {
|
|
1171
|
+
const h = this.host;
|
|
1172
|
+
const s = h.state;
|
|
1173
|
+
return (h.hotkeysEnabled &&
|
|
1174
|
+
!h.hasOpenLayer() &&
|
|
1175
|
+
s.mode === 'base' &&
|
|
1176
|
+
!s.busy);
|
|
1177
|
+
}
|
|
1178
|
+
clearBetTimer() {
|
|
1179
|
+
if (this.betTimer !== null) {
|
|
1180
|
+
clearTimeout(this.betTimer);
|
|
1181
|
+
this.betTimer = null;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
startBetRepeat(dir, elapsed) {
|
|
1185
|
+
// elapsed is ms already spent holding; use it to accelerate toward 45ms floor.
|
|
1186
|
+
// Start at 90ms, decrease ~1ms per 10ms held after the first repeat, floor at 45ms.
|
|
1187
|
+
const interval = Math.max(45, 90 - Math.floor(elapsed / 10));
|
|
1188
|
+
this.betTimer = setTimeout(() => {
|
|
1189
|
+
this.betTimer = null;
|
|
1190
|
+
if (this.betHeldCode !== null && this.isBetAllowed()) {
|
|
1191
|
+
this.host.stepBet(dir);
|
|
1192
|
+
this.startBetRepeat(dir, elapsed + interval);
|
|
1193
|
+
}
|
|
1194
|
+
}, interval);
|
|
1195
|
+
}
|
|
1196
|
+
onKeyDown = (e) => {
|
|
1197
|
+
const target = e.target;
|
|
1198
|
+
// Editable element guard — never intercept keyboard input
|
|
1199
|
+
if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)))
|
|
1200
|
+
return;
|
|
1201
|
+
// For Space: claim preventDefault early (before layer/mode/busy bail) so the browser's
|
|
1202
|
+
// native "Space activates focused button" can't re-fire a shell control and flicker a modal.
|
|
1203
|
+
if (e.code === 'Space' && !e.repeat) {
|
|
1204
|
+
if (!this.host.spacebarEnabled || !this.host.hotkeysEnabled)
|
|
1205
|
+
return;
|
|
1206
|
+
e.preventDefault();
|
|
1207
|
+
if (this.host.hasOpenLayer()) {
|
|
1208
|
+
this.host.routeToLayer(e);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const s = this.host.state;
|
|
1212
|
+
if (s.mode !== 'base' || s.busy || s.autoplay.active)
|
|
1213
|
+
return;
|
|
1214
|
+
this.spaceHeld = true;
|
|
1215
|
+
this.host.spin();
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
// Bet step keys (Shift+arrows, Shift+=/-, NumpadAdd/Subtract) — non-repeat only
|
|
1219
|
+
if (!e.repeat) {
|
|
1220
|
+
const dir = betDir(e);
|
|
1221
|
+
if (dir !== null && this.isBetAllowed()) {
|
|
1222
|
+
this.betHeldCode = e.code;
|
|
1223
|
+
this.host.stepBet(dir);
|
|
1224
|
+
// First repeat after 350ms initial delay
|
|
1225
|
+
this.clearBetTimer();
|
|
1226
|
+
const capturedDir = dir;
|
|
1227
|
+
this.betTimer = setTimeout(() => {
|
|
1228
|
+
this.betTimer = null;
|
|
1229
|
+
if (this.betHeldCode !== null && this.isBetAllowed()) {
|
|
1230
|
+
this.host.stepBet(capturedDir);
|
|
1231
|
+
this.startBetRepeat(capturedDir, 350);
|
|
1232
|
+
}
|
|
1233
|
+
}, 350);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
// Non-Space keys: give the open layer first refusal. If it consumes the key, done; Escape closes
|
|
1238
|
+
// it. Anything the layer does NOT consume falls through to the chrome hotkeys below — so the
|
|
1239
|
+
// Settings/Info pages still honour Shift+I (Game info), Shift+M (sound), Shift+S, etc.
|
|
1240
|
+
if (this.host.hasOpenLayer()) {
|
|
1241
|
+
const consumed = this.host.routeToLayer(e);
|
|
1242
|
+
if (consumed)
|
|
1243
|
+
return;
|
|
1244
|
+
if (e.code === 'Escape') {
|
|
1245
|
+
this.host.closeLayer();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
// not consumed → fall through to the Shift+letter chrome hotkeys
|
|
1249
|
+
}
|
|
1250
|
+
// Shift+letter bar hotkeys — fire when no layer is open, OR when an open layer left the key
|
|
1251
|
+
// unconsumed (see fall-through above); gated on hotkeys being enabled.
|
|
1252
|
+
if (!e.repeat && e.shiftKey && this.host.hotkeysEnabled) {
|
|
1253
|
+
const h = this.host;
|
|
1254
|
+
const s = h.state;
|
|
1255
|
+
switch (e.code) {
|
|
1256
|
+
case 'KeyA':
|
|
1257
|
+
if (h.autoplayEnabled && !s.replay) {
|
|
1258
|
+
h.toggleAutoplay();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
break;
|
|
1262
|
+
case 'KeyT':
|
|
1263
|
+
if (h.turboLevels > 0 && !s.replay) {
|
|
1264
|
+
h.cycleTurbo();
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
break;
|
|
1268
|
+
case 'KeyB':
|
|
1269
|
+
if (h.buyBonusEnabled && s.mode === 'base' && !s.replay) {
|
|
1270
|
+
h.openBuyBonus();
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
break;
|
|
1274
|
+
case 'KeyI':
|
|
1275
|
+
h.openInfo();
|
|
1276
|
+
return;
|
|
1277
|
+
case 'KeyS':
|
|
1278
|
+
h.openMenu();
|
|
1279
|
+
return;
|
|
1280
|
+
case 'KeyM':
|
|
1281
|
+
h.toggleMute();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
onKeyUp = (e) => {
|
|
1287
|
+
if (e.code === 'Space') {
|
|
1288
|
+
this.spaceHeld = false;
|
|
1289
|
+
this.clearHoldTimer();
|
|
1290
|
+
}
|
|
1291
|
+
// Stop bet repeat on key release
|
|
1292
|
+
if (e.code === this.betHeldCode) {
|
|
1293
|
+
this.betHeldCode = null;
|
|
1294
|
+
this.clearBetTimer();
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
onBlur = () => {
|
|
1298
|
+
// Window blur — stop bet repeat AND hold-to-spin (same as releasing both keys)
|
|
1299
|
+
this.betHeldCode = null;
|
|
1300
|
+
this.clearBetTimer();
|
|
1301
|
+
this.spaceHeld = false;
|
|
1302
|
+
this.clearHoldTimer();
|
|
1303
|
+
};
|
|
1304
|
+
clearHoldTimer() {
|
|
1305
|
+
if (this.holdTimer !== null) {
|
|
1306
|
+
clearTimeout(this.holdTimer);
|
|
1307
|
+
this.holdTimer = null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
attach() {
|
|
1311
|
+
this.doc.addEventListener('keydown', this.onKeyDown);
|
|
1312
|
+
this.doc.addEventListener('keyup', this.onKeyUp);
|
|
1313
|
+
// Use window if available for blur events
|
|
1314
|
+
if (typeof window !== 'undefined') {
|
|
1315
|
+
window.addEventListener('blur', this.onBlur);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
detach() {
|
|
1319
|
+
this.doc.removeEventListener('keydown', this.onKeyDown);
|
|
1320
|
+
this.doc.removeEventListener('keyup', this.onKeyUp);
|
|
1321
|
+
if (typeof window !== 'undefined') {
|
|
1322
|
+
window.removeEventListener('blur', this.onBlur);
|
|
1323
|
+
}
|
|
1324
|
+
this.spaceHeld = false;
|
|
1325
|
+
this.clearHoldTimer();
|
|
1326
|
+
this.betHeldCode = null;
|
|
1327
|
+
this.clearBetTimer();
|
|
1328
|
+
}
|
|
1329
|
+
notifyBusyChanged(busy) {
|
|
1330
|
+
if (busy)
|
|
1331
|
+
return;
|
|
1332
|
+
if (!this.spaceHeld)
|
|
1333
|
+
return;
|
|
1334
|
+
if (!this.isSpinAllowed())
|
|
1335
|
+
return;
|
|
1336
|
+
// Schedule the next spin after the 120 ms floor (gap between completion and next spin).
|
|
1337
|
+
this.clearHoldTimer();
|
|
1338
|
+
this.holdTimer = setTimeout(() => {
|
|
1339
|
+
this.holdTimer = null;
|
|
1340
|
+
if (this.spaceHeld && this.isSpinAllowed()) {
|
|
1341
|
+
this.host.spin();
|
|
1342
|
+
}
|
|
1343
|
+
}, 120);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
987
1347
|
function createInitialState(config) {
|
|
988
1348
|
return {
|
|
989
1349
|
mode: config.mode,
|
|
@@ -1166,11 +1526,11 @@ const SHELL_CSS = SHELL_FONT_CSS + `
|
|
|
1166
1526
|
|
|
1167
1527
|
/* host = bottom-anchored flex column: [win pill (on overflow)] above [the bar] */
|
|
1168
1528
|
#${SHELL_ROOT_ID} .ge-shell-barhost { position:absolute; left:0; right:0; bottom:0; pointer-events:none;
|
|
1169
|
-
display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:
|
|
1529
|
+
display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:4px;
|
|
1170
1530
|
transform-origin:bottom center; }
|
|
1171
1531
|
/* bottom bar: transparent, two zones (wide default) */
|
|
1172
1532
|
#${SHELL_ROOT_ID} .ge-shell-bottom { width:100%; box-sizing:border-box; pointer-events:none;
|
|
1173
|
-
display:flex; align-items:center; justify-content:space-between; padding:0 18px
|
|
1533
|
+
display:flex; align-items:center; justify-content:space-between; padding:0 18px 6px; gap:14px; }
|
|
1174
1534
|
#${SHELL_ROOT_ID} .ge-zone { display:flex; align-items:center; gap:14px; pointer-events:none; }
|
|
1175
1535
|
#${SHELL_ROOT_ID} .ge-zone > * { pointer-events:auto; }
|
|
1176
1536
|
#${SHELL_ROOT_ID} .ge-betstep { display:flex; flex-direction:column; gap:2px; }
|
|
@@ -1253,6 +1613,21 @@ const SHELL_CSS = SHELL_FONT_CSS + `
|
|
|
1253
1613
|
#${SHELL_ROOT_ID} .ge-gi-version { text-align:center; color:var(--shell-muted); font-size:11px;
|
|
1254
1614
|
letter-spacing:.08em; opacity:.7; margin:4px 0 2px; }
|
|
1255
1615
|
|
|
1616
|
+
/* hotkeys — keycap chips → localized action label, mirrors controls row layout */
|
|
1617
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-block { display:flex; flex-direction:column; }
|
|
1618
|
+
#${SHELL_ROOT_ID} .ge-gi-hk { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 0; }
|
|
1619
|
+
#${SHELL_ROOT_ID} .ge-gi-hk + .ge-gi-hk { border-top:1px solid var(--shell-plaque-line); }
|
|
1620
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-chips { display:flex; align-items:center; flex-wrap:wrap; gap:4px; flex:0 0 auto; }
|
|
1621
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-combo { display:inline-flex; align-items:center; gap:4px; }
|
|
1622
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-chip { display:inline-flex; align-items:center; justify-content:center;
|
|
1623
|
+
padding:2px 7px; border-radius:6px; border:1px solid var(--shell-plaque-line);
|
|
1624
|
+
background:var(--shell-plaque-dark); color:#fff;
|
|
1625
|
+
font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace; font-size:12px;
|
|
1626
|
+
font-weight:600; line-height:1.5; white-space:nowrap; min-width:1.6em; text-align:center; }
|
|
1627
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-sep { color:var(--shell-plaque-label); font-size:11px; padding:0 1px; }
|
|
1628
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-sep2 { color:var(--shell-plaque-label); font-size:11px; padding:0 4px; }
|
|
1629
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-tx { color:rgba(255,255,255,.88); font-size:14px; font-weight:600; text-align:right; flex:1; }
|
|
1630
|
+
|
|
1256
1631
|
/* controls — two blocks (gameplay / menu & info), icon/name/description per control */
|
|
1257
1632
|
#${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); }
|
|
1258
1633
|
#${SHELL_ROOT_ID} .ge-gi-ctl-block-h { color:var(--shell-plaque-label); font-size:11px; letter-spacing:.12em;
|
|
@@ -1361,6 +1736,8 @@ const SHELL_CSS = SHELL_FONT_CSS + `
|
|
|
1361
1736
|
pointer-events:auto; cursor:pointer; transition:box-shadow .12s ease, background .12s ease; }
|
|
1362
1737
|
#${SHELL_ROOT_ID} .ge-bonus-card:hover:not(.ge-bonus-off) {
|
|
1363
1738
|
box-shadow:0 0 0 1px var(--card-acc), 0 12px 34px -12px var(--card-acc); }
|
|
1739
|
+
#${SHELL_ROOT_ID} .ge-bonus-card--kbd-focus:not(.ge-bonus-off) {
|
|
1740
|
+
box-shadow:0 0 0 1px var(--card-acc), 0 12px 34px -12px var(--card-acc); }
|
|
1364
1741
|
/* custom card (BonusOption.custom): keep grid sizing + accent vars, drop the default chrome so the game owns the UI */
|
|
1365
1742
|
#${SHELL_ROOT_ID} .ge-bonus-card--custom { background:none; border:none; cursor:default; }
|
|
1366
1743
|
#${SHELL_ROOT_ID} .ge-bonus-body { display:flex; flex-direction:column; align-items:center; flex:1; padding:1.25em 1.1em .9em; }
|
|
@@ -1439,7 +1816,7 @@ const SHELL_CSS = SHELL_FONT_CSS + `
|
|
|
1439
1816
|
#${SHELL_ROOT_ID} .ge-pl .ge-iconbtn { color:#fff; }
|
|
1440
1817
|
/* LEFT: [menu] ⊐ coin ⊏ [balance] — coin overlaps both; balance fixed-wide so it doesn't jiggle */
|
|
1441
1818
|
#${SHELL_ROOT_ID} .ge-pl-menu { border-radius:16px 0 0 16px; padding-right:20px; }
|
|
1442
|
-
#${SHELL_ROOT_ID} .ge-pl-bal { border-radius:0 16px 16px 0; padding-left:24px; min-width:
|
|
1819
|
+
#${SHELL_ROOT_ID} .ge-pl-bal { border-radius:0 16px 16px 0; padding-left:24px; min-width:200px; }
|
|
1443
1820
|
#${SHELL_ROOT_ID} .ge-zone-plaques .ge-shell-buybonus { margin:0 -16px; position:relative; z-index:3; }
|
|
1444
1821
|
/* RIGHT: [bet] · |divider| · [auto · SPIN · turbo] */
|
|
1445
1822
|
#${SHELL_ROOT_ID} .ge-pl-bet { border-radius:16px 0 0 16px; justify-content:space-between;
|
|
@@ -1639,7 +2016,8 @@ function createCardModal(opts) {
|
|
|
1639
2016
|
}
|
|
1640
2017
|
return { root, card, body };
|
|
1641
2018
|
}
|
|
1642
|
-
/** Full-screen overlay. Returns { root, body }; append content to body.
|
|
2019
|
+
/** Full-screen overlay. Returns { root, body, scroll }; append content to body.
|
|
2020
|
+
* The `scroll` element is the scrollable container (overflow-y: auto). */
|
|
1643
2021
|
function createOverlay(opts) {
|
|
1644
2022
|
const root = document.createElement('div');
|
|
1645
2023
|
root.className = 'ge-shell-overlay';
|
|
@@ -1677,7 +2055,7 @@ function createOverlay(opts) {
|
|
|
1677
2055
|
body.className = 'ge-ov-body';
|
|
1678
2056
|
scroll.appendChild(body);
|
|
1679
2057
|
root.append(head, scroll);
|
|
1680
|
-
return { root, body };
|
|
2058
|
+
return { root, body, scroll };
|
|
1681
2059
|
}
|
|
1682
2060
|
|
|
1683
2061
|
/** A floating labelled money readout (balance/win/bet). */
|
|
@@ -1929,24 +2307,22 @@ function applyBusy(shell, bar) {
|
|
|
1929
2307
|
function openSettingsModal(shell) {
|
|
1930
2308
|
const { root, body } = createOverlay({ title: shell.t('Settings'), onClose: () => root.remove() });
|
|
1931
2309
|
root.dataset.ge = 'settings-modal';
|
|
1932
|
-
// Sound on/off
|
|
2310
|
+
// Sound on/off — backed by the shell's shared `soundOn` state so this toggle and the Shift+M
|
|
2311
|
+
// hotkey stay in sync; `setSound` emits `settingChange({ key: 'sound' })` and refreshes the icon.
|
|
1933
2312
|
const sound = (() => {
|
|
1934
|
-
let on = true;
|
|
1935
2313
|
const btn = document.createElement('button');
|
|
1936
|
-
btn.className = 'ge-snd
|
|
2314
|
+
btn.className = 'ge-snd';
|
|
1937
2315
|
btn.dataset.ge = 'setting-sound';
|
|
1938
|
-
btn.setAttribute('aria-label', 'Sound');
|
|
1939
|
-
const paint = () => {
|
|
2316
|
+
btn.setAttribute('aria-label', shell.t('Sound'));
|
|
2317
|
+
const paint = (on) => {
|
|
1940
2318
|
btn.innerHTML = icon(on ? 'soundOn' : 'soundOff');
|
|
1941
2319
|
btn.classList.toggle('ge-active', on);
|
|
1942
2320
|
btn.setAttribute('aria-pressed', String(on));
|
|
1943
2321
|
};
|
|
1944
|
-
paint();
|
|
1945
|
-
btn.addEventListener('click', () =>
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
shell.emit('settingChange', { key: 'sound', value: on });
|
|
1949
|
-
});
|
|
2322
|
+
paint(shell.soundOn);
|
|
2323
|
+
btn.addEventListener('click', () => shell.setSound(!shell.soundOn));
|
|
2324
|
+
// Live-update the icon when sound changes from here OR via Shift+M (shell clears on close).
|
|
2325
|
+
shell.setSoundRefresh(paint);
|
|
1950
2326
|
const row = document.createElement('div');
|
|
1951
2327
|
row.className = 'ge-ov-row';
|
|
1952
2328
|
row.innerHTML = `<span class="ge-grow">${shell.t('Sound')}</span>`;
|
|
@@ -1996,17 +2372,25 @@ function openSettingsModal(shell) {
|
|
|
1996
2372
|
|
|
1997
2373
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1998
2374
|
/** The @energy8platform/platform-core package version, stamped at build time. */
|
|
1999
|
-
const PACKAGE_VERSION = '0.
|
|
2375
|
+
const PACKAGE_VERSION = '0.26.0';
|
|
2000
2376
|
|
|
2377
|
+
/** Default order key for the auto-injected hotkeys section: just after `controls` (-1). */
|
|
2378
|
+
const HOTKEYS_DEFAULT_ORDER = -0.5;
|
|
2001
2379
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
2002
2380
|
function openGameInfoModal(shell) {
|
|
2003
|
-
const { root, body } = createOverlay({
|
|
2381
|
+
const { root, body, scroll } = createOverlay({
|
|
2004
2382
|
title: shell.t('Game info'),
|
|
2005
2383
|
onClose: () => root.remove(),
|
|
2006
2384
|
onBack: () => { root.remove(); shell.openSettings(); },
|
|
2007
2385
|
});
|
|
2008
2386
|
root.dataset.ge = 'info-modal';
|
|
2009
|
-
const
|
|
2387
|
+
const rawSections = shell.config.gameInfo.sections ?? [];
|
|
2388
|
+
// Auto-inject a hotkeys section unless the game already provides one or features.hotkeys === false.
|
|
2389
|
+
const sectionsWithHotkeys = [...rawSections];
|
|
2390
|
+
if (shell.config.features.hotkeys !== false && !rawSections.some((s) => s.type === 'hotkeys')) {
|
|
2391
|
+
sectionsWithHotkeys.push({ type: 'hotkeys', order: HOTKEYS_DEFAULT_ORDER });
|
|
2392
|
+
}
|
|
2393
|
+
const sections = sectionsWithHotkeys;
|
|
2010
2394
|
// Default placement: modes first, controls second, the rest in declaration order.
|
|
2011
2395
|
// An explicit `order` overrides; ties keep declaration order (stable).
|
|
2012
2396
|
const base = (s, i) => s.order ?? (s.type === 'modes' ? -2 : s.type === 'controls' ? -1 : i);
|
|
@@ -2015,7 +2399,40 @@ function openGameInfoModal(shell) {
|
|
|
2015
2399
|
.sort((a, b) => a.k - b.k || a.i - b.i)
|
|
2016
2400
|
.forEach(({ s }) => body.appendChild(renderSection(shell, s)));
|
|
2017
2401
|
body.appendChild(versionFooter(shell));
|
|
2018
|
-
|
|
2402
|
+
const LINE = 60;
|
|
2403
|
+
const PAGE = () => Math.floor(scroll.clientHeight * 0.9) || Math.floor(540 * 0.9);
|
|
2404
|
+
const onKey = (e) => {
|
|
2405
|
+
switch (e.code) {
|
|
2406
|
+
case 'ArrowDown':
|
|
2407
|
+
scroll.scrollTop += LINE;
|
|
2408
|
+
return true;
|
|
2409
|
+
case 'ArrowUp':
|
|
2410
|
+
scroll.scrollTop = Math.max(0, scroll.scrollTop - LINE);
|
|
2411
|
+
return true;
|
|
2412
|
+
case 'PageDown':
|
|
2413
|
+
scroll.scrollTop += PAGE();
|
|
2414
|
+
return true;
|
|
2415
|
+
case 'PageUp':
|
|
2416
|
+
scroll.scrollTop = Math.max(0, scroll.scrollTop - PAGE());
|
|
2417
|
+
return true;
|
|
2418
|
+
case 'Space':
|
|
2419
|
+
if (e.shiftKey) {
|
|
2420
|
+
scroll.scrollTop = Math.max(0, scroll.scrollTop - PAGE());
|
|
2421
|
+
}
|
|
2422
|
+
else {
|
|
2423
|
+
scroll.scrollTop += PAGE();
|
|
2424
|
+
}
|
|
2425
|
+
return true;
|
|
2426
|
+
case 'Home':
|
|
2427
|
+
scroll.scrollTop = 0;
|
|
2428
|
+
return true;
|
|
2429
|
+
case 'End':
|
|
2430
|
+
scroll.scrollTop = scroll.scrollHeight - scroll.clientHeight;
|
|
2431
|
+
return true;
|
|
2432
|
+
default: return false;
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
return { root, onKey };
|
|
2019
2436
|
}
|
|
2020
2437
|
/** A muted version stamp pinned to the bottom of the game-info modal:
|
|
2021
2438
|
* `${config.version ?? '1.0.0'}.${engine version without dots}` (e.g. '1.0.0.0246'). */
|
|
@@ -2031,9 +2448,11 @@ function renderSection(shell, s) {
|
|
|
2031
2448
|
switch (s.type) {
|
|
2032
2449
|
case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
|
|
2033
2450
|
case 'controls': return sectionControls(shell, sec('info-controls', s.title, shell.t('Controls')));
|
|
2451
|
+
case 'hotkeys': return sectionHotkeys(shell, sec('info-hotkeys', s.title, shell.t('Hotkeys')));
|
|
2034
2452
|
case 'paytable': return sectionPaytable(s.rows, sec('info-paytable', s.title, shell.t('Paytable')));
|
|
2035
2453
|
case 'wins': return sectionWins(s, sec('info-wins', s.title, shell.t(winFallbackTitle(s.kind))));
|
|
2036
|
-
|
|
2454
|
+
// Translate the heading (e.g. the host-built DISCLAIMER title); the body stays verbatim.
|
|
2455
|
+
case 'custom': return sectionCustom(s, sec('info-custom', s.title != null ? shell.t(s.title) : undefined, ''));
|
|
2037
2456
|
}
|
|
2038
2457
|
}
|
|
2039
2458
|
/** A titled glass-plaque section shell. */
|
|
@@ -2115,6 +2534,57 @@ function ctlBlock(shell, label, rows) {
|
|
|
2115
2534
|
}
|
|
2116
2535
|
return block;
|
|
2117
2536
|
}
|
|
2537
|
+
function sectionHotkeys(shell, el) {
|
|
2538
|
+
const { features } = shell.config;
|
|
2539
|
+
/** Render one or more key names as keycap chips joined by " / ". */
|
|
2540
|
+
const chips = (...keys) => keys.map((k) => `<span class="ge-gi-hk-chip">${k}</span>`).join('<span class="ge-gi-hk-sep"> / </span>');
|
|
2541
|
+
const rows = [
|
|
2542
|
+
{ chips: ['Space'], name: 'Spin', on: true },
|
|
2543
|
+
{ chips: ['Shift', '↑', 'Shift', '='], name: 'Raise bet', on: true },
|
|
2544
|
+
{ chips: ['Shift', '↓', 'Shift', '-'], name: 'Lower bet', on: true },
|
|
2545
|
+
{ chips: ['Shift', 'A'], name: 'Autoplay', on: features.autoplay != null },
|
|
2546
|
+
{ chips: ['Shift', 'T'], name: 'Turbo', on: features.turbo > 0 },
|
|
2547
|
+
{ chips: ['Shift', 'B'], name: 'Buy bonus', on: features.buyBonus !== false },
|
|
2548
|
+
{ chips: ['Shift', 'I'], name: 'Game info', on: true },
|
|
2549
|
+
{ chips: ['Shift', 'S'], name: 'Menu', on: true },
|
|
2550
|
+
{ chips: ['Shift', 'M'], name: 'Mute', on: true },
|
|
2551
|
+
{ chips: ['←', '→'], name: 'Navigate', on: true },
|
|
2552
|
+
{ chips: ['Enter'], name: 'Confirm', on: true },
|
|
2553
|
+
{ chips: ['Esc'], name: 'Close', on: true },
|
|
2554
|
+
];
|
|
2555
|
+
const block = document.createElement('div');
|
|
2556
|
+
block.className = 'ge-gi-hk-block';
|
|
2557
|
+
for (const r of rows.filter((x) => x.on)) {
|
|
2558
|
+
const row = document.createElement('div');
|
|
2559
|
+
row.className = 'ge-gi-hk';
|
|
2560
|
+
// Build the chips column
|
|
2561
|
+
const chipsEl = document.createElement('div');
|
|
2562
|
+
chipsEl.className = 'ge-gi-hk-chips';
|
|
2563
|
+
if (r.name === 'Raise bet' || r.name === 'Lower bet') {
|
|
2564
|
+
// Two combos separated by " / ": Shift+↑ / Shift+= and Shift+↓ / Shift+-
|
|
2565
|
+
const [k1, k2, k3, k4] = r.chips;
|
|
2566
|
+
chipsEl.innerHTML =
|
|
2567
|
+
`<span class="ge-gi-hk-combo">${chips(k1, k2)}</span>` +
|
|
2568
|
+
`<span class="ge-gi-hk-sep2"> / </span>` +
|
|
2569
|
+
`<span class="ge-gi-hk-combo">${chips(k3, k4)}</span>`;
|
|
2570
|
+
}
|
|
2571
|
+
else if (r.chips.length > 1) {
|
|
2572
|
+
// Chord: Shift + X
|
|
2573
|
+
chipsEl.innerHTML = `<span class="ge-gi-hk-combo">${chips(...r.chips)}</span>`;
|
|
2574
|
+
}
|
|
2575
|
+
else {
|
|
2576
|
+
chipsEl.innerHTML = chips(...r.chips);
|
|
2577
|
+
}
|
|
2578
|
+
const tx = document.createElement('div');
|
|
2579
|
+
tx.className = 'ge-gi-hk-tx';
|
|
2580
|
+
tx.textContent = shell.t(r.name);
|
|
2581
|
+
row.appendChild(chipsEl);
|
|
2582
|
+
row.appendChild(tx);
|
|
2583
|
+
block.appendChild(row);
|
|
2584
|
+
}
|
|
2585
|
+
el.appendChild(block);
|
|
2586
|
+
return el;
|
|
2587
|
+
}
|
|
2118
2588
|
// ── paytable (cards — image on top, name, then win tiers "<count> x<mult>") ────
|
|
2119
2589
|
function sectionPaytable(rows, el) {
|
|
2120
2590
|
const grid = document.createElement('div');
|
|
@@ -2313,25 +2783,165 @@ function sectionCustom(s, el) {
|
|
|
2313
2783
|
return el;
|
|
2314
2784
|
}
|
|
2315
2785
|
|
|
2316
|
-
/** Buy-bonus overlay — a grid of art-forward cards, one per option.
|
|
2786
|
+
/** Buy-bonus overlay — a grid of art-forward cards, one per option.
|
|
2787
|
+
* Returns the overlay element + a keyboard handler for the shell's `showModal`. */
|
|
2317
2788
|
function openBuyBonusOverlay(shell) {
|
|
2318
2789
|
const bonuses = shell.config.features.buyBonus;
|
|
2319
2790
|
if (bonuses === false || bonuses.length === 0)
|
|
2320
2791
|
return null;
|
|
2321
|
-
const
|
|
2792
|
+
const st = { focusIndex: -1, confirmBonus: undefined };
|
|
2793
|
+
const { root, body } = createOverlay({ title: shell.t('Buy bonus'), onClose: () => shell.closeModal() });
|
|
2322
2794
|
root.dataset.ge = 'buybonus-overlay';
|
|
2323
2795
|
// Re-render the grid whenever the bet changes so every card's price stays live.
|
|
2324
2796
|
const renderGrid = () => {
|
|
2325
2797
|
body.innerHTML = '';
|
|
2326
2798
|
const grid = document.createElement('div');
|
|
2327
2799
|
grid.className = 'ge-bb-grid';
|
|
2328
|
-
|
|
2329
|
-
|
|
2800
|
+
const affordable = [];
|
|
2801
|
+
for (const bonus of bonuses) {
|
|
2802
|
+
const card = buildCard(shell, bonus, root, st);
|
|
2803
|
+
grid.appendChild(card);
|
|
2804
|
+
if (isAffordable(shell, bonus))
|
|
2805
|
+
affordable.push(bonus);
|
|
2806
|
+
}
|
|
2330
2807
|
body.appendChild(grid);
|
|
2808
|
+
// Initialize or restore focus index
|
|
2809
|
+
if (affordable.length > 0) {
|
|
2810
|
+
if (st.focusIndex < 0)
|
|
2811
|
+
st.focusIndex = 0;
|
|
2812
|
+
else
|
|
2813
|
+
st.focusIndex = Math.min(st.focusIndex, affordable.length - 1);
|
|
2814
|
+
applyFocusClass(root, bonuses, affordable, st.focusIndex);
|
|
2815
|
+
}
|
|
2816
|
+
else {
|
|
2817
|
+
st.focusIndex = -1;
|
|
2818
|
+
}
|
|
2331
2819
|
};
|
|
2332
2820
|
renderGrid();
|
|
2333
2821
|
root.appendChild(buildBetBar(shell, renderGrid)); // thin bottom footer, only as tall as the pill
|
|
2334
|
-
|
|
2822
|
+
/** Step the bet by `dir` and re-render the grid (live prices + affordability) when it changed.
|
|
2823
|
+
* Shared by the keyboard bet keys (the footer ± buttons keep their own copy). */
|
|
2824
|
+
const stepBetBy = (dir) => {
|
|
2825
|
+
const next = stepBet(shell.state, dir);
|
|
2826
|
+
if (next === shell.state.bet)
|
|
2827
|
+
return;
|
|
2828
|
+
shell.state.bet = next;
|
|
2829
|
+
shell.emit('betChange', next);
|
|
2830
|
+
shell.render();
|
|
2831
|
+
renderGrid();
|
|
2832
|
+
};
|
|
2833
|
+
/** Keyboard handler for both browse and confirm phases. */
|
|
2834
|
+
const onKey = (e) => {
|
|
2835
|
+
const affordable = bonuses.filter((b) => isAffordable(shell, b));
|
|
2836
|
+
// ── Confirm phase ──
|
|
2837
|
+
if (st.confirmBonus) {
|
|
2838
|
+
switch (e.code) {
|
|
2839
|
+
case 'Enter':
|
|
2840
|
+
case 'Space': {
|
|
2841
|
+
const bonus = st.confirmBonus;
|
|
2842
|
+
if (!isAffordable(shell, bonus))
|
|
2843
|
+
return true;
|
|
2844
|
+
if (bonus.type === 'feature')
|
|
2845
|
+
shell.activateFeature(bonus);
|
|
2846
|
+
else
|
|
2847
|
+
shell.emit('buyBonusSelect', { id: bonus.id });
|
|
2848
|
+
shell.closeModal();
|
|
2849
|
+
return true;
|
|
2850
|
+
}
|
|
2851
|
+
case 'Escape':
|
|
2852
|
+
// Remove the confirm dialog, return to browse
|
|
2853
|
+
closeConfirm(root, st);
|
|
2854
|
+
return true;
|
|
2855
|
+
default:
|
|
2856
|
+
return false;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
// ── Browse phase ──
|
|
2860
|
+
const last = affordable.length - 1;
|
|
2861
|
+
const mobile = shell.layout === 'mobile';
|
|
2862
|
+
// Bet stepping mirrors the bar's keys (Shift+↑/↓, Shift+=/-, Numpad ±). Checked BEFORE arrow
|
|
2863
|
+
// navigation so a bare arrow still moves card focus while a Shift+arrow changes the bet.
|
|
2864
|
+
const bet = betDir(e);
|
|
2865
|
+
if (bet !== null) {
|
|
2866
|
+
stepBetBy(bet);
|
|
2867
|
+
return true;
|
|
2868
|
+
}
|
|
2869
|
+
// Determine navigation direction from key code + layout (mobile uses vertical arrows)
|
|
2870
|
+
const fwdKey = e.code === 'ArrowRight' || (mobile && e.code === 'ArrowDown');
|
|
2871
|
+
const bwdKey = e.code === 'ArrowLeft' || (mobile && e.code === 'ArrowUp');
|
|
2872
|
+
if (fwdKey) {
|
|
2873
|
+
if (last < 0)
|
|
2874
|
+
return true;
|
|
2875
|
+
if (st.focusIndex < last) {
|
|
2876
|
+
st.focusIndex++;
|
|
2877
|
+
applyFocusClass(root, bonuses, affordable, st.focusIndex);
|
|
2878
|
+
}
|
|
2879
|
+
return true;
|
|
2880
|
+
}
|
|
2881
|
+
if (bwdKey) {
|
|
2882
|
+
if (last < 0)
|
|
2883
|
+
return true;
|
|
2884
|
+
if (st.focusIndex > 0) {
|
|
2885
|
+
st.focusIndex--;
|
|
2886
|
+
applyFocusClass(root, bonuses, affordable, st.focusIndex);
|
|
2887
|
+
}
|
|
2888
|
+
return true;
|
|
2889
|
+
}
|
|
2890
|
+
switch (e.code) {
|
|
2891
|
+
case 'Enter':
|
|
2892
|
+
case 'Space':
|
|
2893
|
+
if (last < 0 || st.focusIndex < 0)
|
|
2894
|
+
return true;
|
|
2895
|
+
{
|
|
2896
|
+
const bonus = affordable[st.focusIndex];
|
|
2897
|
+
openConfirm(shell, bonus, root, st);
|
|
2898
|
+
}
|
|
2899
|
+
return true;
|
|
2900
|
+
// Bare =/- also step the bet (the Shift+=/- and Numpad variants are handled by betDir above).
|
|
2901
|
+
case 'Equal':
|
|
2902
|
+
stepBetBy(1);
|
|
2903
|
+
return true;
|
|
2904
|
+
case 'Minus':
|
|
2905
|
+
stepBetBy(-1);
|
|
2906
|
+
return true;
|
|
2907
|
+
case 'Escape':
|
|
2908
|
+
shell.closeModal();
|
|
2909
|
+
return true;
|
|
2910
|
+
default:
|
|
2911
|
+
return false;
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
return { root, onKey };
|
|
2915
|
+
}
|
|
2916
|
+
/** Apply a CSS keyboard-focus class to the currently focused affordable card. */
|
|
2917
|
+
function applyFocusClass(overlay, bonuses, affordable, focusIndex) {
|
|
2918
|
+
for (const b of bonuses) {
|
|
2919
|
+
const card = overlay.querySelector(`[data-ge="bonus-card-${b.id}"]`);
|
|
2920
|
+
if (!card)
|
|
2921
|
+
continue;
|
|
2922
|
+
card.classList.remove('ge-bonus-card--kbd-focus');
|
|
2923
|
+
}
|
|
2924
|
+
const focused = affordable[focusIndex];
|
|
2925
|
+
if (!focused)
|
|
2926
|
+
return;
|
|
2927
|
+
const card = overlay.querySelector(`[data-ge="bonus-card-${focused.id}"]`);
|
|
2928
|
+
if (card)
|
|
2929
|
+
card.classList.add('ge-bonus-card--kbd-focus');
|
|
2930
|
+
}
|
|
2931
|
+
/** Open the confirm dialog for the given bonus and track it in overlay state. */
|
|
2932
|
+
function openConfirm(shell, bonus, overlay, st) {
|
|
2933
|
+
closeConfirm(overlay, st); // remove any existing confirm
|
|
2934
|
+
st.confirmBonus = bonus;
|
|
2935
|
+
overlay.appendChild(buildConfirm(shell, bonus, overlay, st));
|
|
2936
|
+
shell.fitModals();
|
|
2937
|
+
}
|
|
2938
|
+
/** Remove the confirm dialog and clear the overlay state. */
|
|
2939
|
+
function closeConfirm(overlay, st) {
|
|
2940
|
+
// The confirm dialog is a .ge-sheet with data-ge="bonus-confirm" appended directly to overlay.
|
|
2941
|
+
const sheet = overlay.querySelector('[data-ge="bonus-confirm"]');
|
|
2942
|
+
if (sheet)
|
|
2943
|
+
sheet.remove();
|
|
2944
|
+
st.confirmBonus = undefined;
|
|
2335
2945
|
}
|
|
2336
2946
|
/** Bet control — a compact −/+ pill around the live stake, in a thin footer at the screen bottom.
|
|
2337
2947
|
* Stepping repaints the value, re-prices the cards, and updates the control bar. */
|
|
@@ -2378,7 +2988,7 @@ function stepButton(ge, name) {
|
|
|
2378
2988
|
}
|
|
2379
2989
|
/** A grid card: title → thumbnail → description → volatility → price → full-bleed CTA.
|
|
2380
2990
|
* Clicking (when affordable) opens the confirmation modal. */
|
|
2381
|
-
function buildCard(shell, bonus, overlay) {
|
|
2991
|
+
function buildCard(shell, bonus, overlay, st) {
|
|
2382
2992
|
const accent = effectiveAccent(bonus);
|
|
2383
2993
|
const card = document.createElement('div');
|
|
2384
2994
|
card.className = 'ge-bonus-card';
|
|
@@ -2391,8 +3001,7 @@ function buildCard(shell, bonus, overlay) {
|
|
|
2391
3001
|
const select = () => {
|
|
2392
3002
|
if (!isAffordable(shell, bonus))
|
|
2393
3003
|
return;
|
|
2394
|
-
|
|
2395
|
-
shell.fitModals();
|
|
3004
|
+
openConfirm(shell, bonus, overlay, st);
|
|
2396
3005
|
};
|
|
2397
3006
|
// Game-supplied card UI: the shell keeps the wrapper (grid sizing + accent vars) and runs the
|
|
2398
3007
|
// buy flow when the game calls ctx.select(); the game owns everything inside.
|
|
@@ -2437,9 +3046,9 @@ function cardBody(shell, bonus) {
|
|
|
2437
3046
|
}
|
|
2438
3047
|
/** Confirmation modal — the shared card chrome (accent title heading, no ✕) with a bonus
|
|
2439
3048
|
* preview body and a full-bleed Cancel + action footer. */
|
|
2440
|
-
function buildConfirm(shell, bonus, overlay) {
|
|
3049
|
+
function buildConfirm(shell, bonus, overlay, st) {
|
|
2441
3050
|
const accent = effectiveAccent(bonus);
|
|
2442
|
-
const ui = createCardModal({ ge: 'bonus-confirm', title: bonus.title, accent, onClose: () =>
|
|
3051
|
+
const ui = createCardModal({ ge: 'bonus-confirm', title: bonus.title, accent, onClose: () => { closeConfirm(overlay, st); } });
|
|
2443
3052
|
const price = bonus.priceMultiplier * shell.state.bet;
|
|
2444
3053
|
const preview = document.createElement('div');
|
|
2445
3054
|
preview.className = 'ge-confirm-preview';
|
|
@@ -2455,7 +3064,7 @@ function buildConfirm(shell, bonus, overlay) {
|
|
|
2455
3064
|
cancel.className = 'ge-modal-btn ge-modal-btn--ghost';
|
|
2456
3065
|
cancel.dataset.ge = 'bonus-confirm-cancel';
|
|
2457
3066
|
cancel.textContent = shell.t('Cancel');
|
|
2458
|
-
cancel.addEventListener('click', () =>
|
|
3067
|
+
cancel.addEventListener('click', () => closeConfirm(overlay, st));
|
|
2459
3068
|
const buy = document.createElement('button');
|
|
2460
3069
|
buy.className = 'ge-modal-btn ge-modal-btn--accent';
|
|
2461
3070
|
buy.dataset.ge = 'bonus-confirm-buy';
|
|
@@ -2470,8 +3079,7 @@ function buildConfirm(shell, bonus, overlay) {
|
|
|
2470
3079
|
shell.activateFeature(bonus);
|
|
2471
3080
|
else
|
|
2472
3081
|
shell.emit('buyBonusSelect', { id: bonus.id });
|
|
2473
|
-
|
|
2474
|
-
overlay.remove();
|
|
3082
|
+
shell.closeModal();
|
|
2475
3083
|
});
|
|
2476
3084
|
actions.append(cancel, buy);
|
|
2477
3085
|
ui.card.appendChild(actions);
|
|
@@ -2500,36 +3108,79 @@ function isAffordable(shell, bonus) {
|
|
|
2500
3108
|
|
|
2501
3109
|
/** A centred picker (chips grid + accent Confirm) on the shared card modal. */
|
|
2502
3110
|
function buildSheet(opts) {
|
|
2503
|
-
const ui = createCardModal({ ge: opts.ge, title: opts.title, onClose: () =>
|
|
3111
|
+
const ui = createCardModal({ ge: opts.ge, title: opts.title, onClose: () => opts.onClose() });
|
|
2504
3112
|
const grid = document.createElement('div');
|
|
2505
3113
|
grid.className = 'ge-sheet-grid';
|
|
2506
3114
|
const cols = typeof opts.columns === 'number' ? { wide: opts.columns, mobile: opts.columns } : opts.columns;
|
|
2507
3115
|
grid.style.setProperty('--cols', String(cols.wide));
|
|
2508
3116
|
grid.style.setProperty('--cols-m', String(cols.mobile));
|
|
2509
3117
|
let selected = opts.selected;
|
|
3118
|
+
let focusIndex = opts.choices.findIndex((c) => c.id === selected);
|
|
3119
|
+
if (focusIndex < 0)
|
|
3120
|
+
focusIndex = 0;
|
|
2510
3121
|
const chips = [];
|
|
2511
|
-
|
|
3122
|
+
/** Update chip visuals to reflect the current selected/focused index. */
|
|
3123
|
+
function setHighlight(newIndex) {
|
|
3124
|
+
focusIndex = newIndex;
|
|
3125
|
+
selected = opts.choices[focusIndex].id;
|
|
3126
|
+
for (let i = 0; i < chips.length; i++) {
|
|
3127
|
+
chips[i].classList.toggle('ge-on', i === focusIndex);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
for (let i = 0; i < opts.choices.length; i++) {
|
|
3131
|
+
const c = opts.choices[i];
|
|
2512
3132
|
const chip = document.createElement('button');
|
|
2513
|
-
chip.className = 'ge-chip' + (
|
|
3133
|
+
chip.className = 'ge-chip' + (i === focusIndex ? ' ge-on' : '');
|
|
2514
3134
|
chip.dataset.id = c.id;
|
|
2515
3135
|
chip.textContent = c.label;
|
|
3136
|
+
const idx = i; // capture for closure
|
|
2516
3137
|
chip.addEventListener('click', () => {
|
|
2517
|
-
|
|
2518
|
-
for (const x of chips)
|
|
2519
|
-
x.classList.toggle('ge-on', x.dataset.id === selected);
|
|
3138
|
+
setHighlight(idx);
|
|
2520
3139
|
});
|
|
2521
3140
|
chips.push(chip);
|
|
2522
3141
|
grid.appendChild(chip);
|
|
2523
3142
|
}
|
|
2524
3143
|
ui.body.appendChild(grid);
|
|
3144
|
+
function doConfirm() {
|
|
3145
|
+
opts.onConfirm(selected);
|
|
3146
|
+
opts.onClose();
|
|
3147
|
+
}
|
|
2525
3148
|
// Single full-bleed Confirm; dismissal is the ✕ (top-right). No Cancel button.
|
|
2526
3149
|
const confirm = document.createElement('button');
|
|
2527
3150
|
confirm.className = 'ge-modal-btn ge-modal-btn--accent';
|
|
2528
3151
|
confirm.dataset.ge = 'sheet-confirm';
|
|
2529
3152
|
confirm.textContent = opts.confirmLabel;
|
|
2530
|
-
confirm.addEventListener('click',
|
|
3153
|
+
confirm.addEventListener('click', doConfirm);
|
|
2531
3154
|
ui.card.appendChild(confirm);
|
|
2532
|
-
|
|
3155
|
+
function onKey(e) {
|
|
3156
|
+
const last = opts.choices.length - 1;
|
|
3157
|
+
switch (e.code) {
|
|
3158
|
+
case 'ArrowRight':
|
|
3159
|
+
case 'ArrowDown':
|
|
3160
|
+
case 'Equal': // + on most keyboards
|
|
3161
|
+
case 'NumpadAdd':
|
|
3162
|
+
if (focusIndex < last)
|
|
3163
|
+
setHighlight(focusIndex + 1);
|
|
3164
|
+
return true;
|
|
3165
|
+
case 'ArrowLeft':
|
|
3166
|
+
case 'ArrowUp':
|
|
3167
|
+
case 'Minus':
|
|
3168
|
+
case 'NumpadSubtract':
|
|
3169
|
+
if (focusIndex > 0)
|
|
3170
|
+
setHighlight(focusIndex - 1);
|
|
3171
|
+
return true;
|
|
3172
|
+
case 'Enter':
|
|
3173
|
+
case 'Space':
|
|
3174
|
+
doConfirm();
|
|
3175
|
+
return true;
|
|
3176
|
+
case 'Escape':
|
|
3177
|
+
opts.onClose();
|
|
3178
|
+
return true;
|
|
3179
|
+
default:
|
|
3180
|
+
return false;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
return { root: ui.root, onKey };
|
|
2533
3184
|
}
|
|
2534
3185
|
/** Bet picker — all available bets as chips (6 per row, 3 on mobile), accent Confirm applies it. */
|
|
2535
3186
|
function openBetModal(shell) {
|
|
@@ -2537,6 +3188,7 @@ function openBetModal(shell) {
|
|
|
2537
3188
|
ge: 'bet-modal', title: shell.t('Bet'), columns: { wide: 6, mobile: 3 }, confirmLabel: shell.t('Confirm'),
|
|
2538
3189
|
choices: shell.state.availableBets.map((b) => ({ id: String(b), label: formatCurrency(b, shell.config.currency) })),
|
|
2539
3190
|
selected: String(shell.state.bet),
|
|
3191
|
+
onClose: () => shell.closeModal(),
|
|
2540
3192
|
onConfirm: (id) => {
|
|
2541
3193
|
const v = Number(id);
|
|
2542
3194
|
if (v !== shell.state.bet) {
|
|
@@ -2567,6 +3219,7 @@ function openAutoplayModal(shell) {
|
|
|
2567
3219
|
ge: 'autoplay-modal', title: shell.t('Autoplay'), columns: 3, confirmLabel: shell.t('Start'),
|
|
2568
3220
|
choices: counts.map((n) => ({ id: String(n), label: Number.isFinite(n) ? String(n) : '∞' })),
|
|
2569
3221
|
selected: String(shell.state.autoplay.remaining || counts[0]),
|
|
3222
|
+
onClose: () => shell.closeModal(),
|
|
2570
3223
|
onConfirm: (id) => {
|
|
2571
3224
|
let remaining = Number(id); // "Infinity" → Infinity
|
|
2572
3225
|
if (maxCount != null)
|
|
@@ -2714,6 +3367,868 @@ function countUp(el, from, to, fmt, durationMs = 450) {
|
|
|
2714
3367
|
};
|
|
2715
3368
|
}
|
|
2716
3369
|
|
|
3370
|
+
// MACHINE-TRANSLATED — native QA required before production use.
|
|
3371
|
+
// Keys are the English source strings exactly as they appear at t('…') call sites.
|
|
3372
|
+
// Entries within each language block are sorted alphabetically by key for diff stability.
|
|
3373
|
+
// 'en' is omitted: the resolver returns the source string unchanged for English.
|
|
3374
|
+
const LOCALES = {
|
|
3375
|
+
da: {
|
|
3376
|
+
DISCLAIMER: 'Ansvarsfraskrivelse',
|
|
3377
|
+
Activate: 'Aktivér',
|
|
3378
|
+
Autoplay: 'Autoplay',
|
|
3379
|
+
Balance: 'Saldo',
|
|
3380
|
+
Bet: 'Indsats',
|
|
3381
|
+
'BUY BONUS': 'KØB BONUS',
|
|
3382
|
+
Buy: 'Køb',
|
|
3383
|
+
'Buy bonus': 'Køb bonus',
|
|
3384
|
+
Cancel: 'Annuller',
|
|
3385
|
+
Close: 'Luk',
|
|
3386
|
+
'Cluster pays': 'Klyngeudbetaling',
|
|
3387
|
+
Confirm: 'Bekræft',
|
|
3388
|
+
Controls: 'Kontroller',
|
|
3389
|
+
'Decrease your stake.': 'Reducer din indsats.',
|
|
3390
|
+
DISABLE: 'DEAKTIVER',
|
|
3391
|
+
'Dismiss the current overlay.': 'Luk det aktuelle overlay.',
|
|
3392
|
+
'Free spins': 'Gratisspins',
|
|
3393
|
+
Game: 'Spil',
|
|
3394
|
+
'Game info': 'Spilinfo',
|
|
3395
|
+
Hotkeys: 'Tastaturgenveje',
|
|
3396
|
+
'Increase your stake.': 'Forøg din indsats.',
|
|
3397
|
+
'Lower bet': 'Lavere indsats',
|
|
3398
|
+
'Master volume': 'Hovedvolumen',
|
|
3399
|
+
'Max win': 'Maks gevinst',
|
|
3400
|
+
Menu: 'Menu',
|
|
3401
|
+
'Menu & info': 'Menu og info',
|
|
3402
|
+
Modes: 'Tilstande',
|
|
3403
|
+
Music: 'Musik',
|
|
3404
|
+
Mute: 'Lydløs',
|
|
3405
|
+
'Mute or unmute the game.': 'Slå lyden til/fra.',
|
|
3406
|
+
Navigate: 'Naviger',
|
|
3407
|
+
'Open settings and game info.': 'Åbn indstillinger og spilinfo.',
|
|
3408
|
+
'Open the paytable and rules.': 'Åbn gevinsttabel og regler.',
|
|
3409
|
+
'Pay a fixed cost to enter a bonus feature.': 'Betal en fast pris for at aktivere en bonusfunktion.',
|
|
3410
|
+
Paylines: 'Gevinstlinjer',
|
|
3411
|
+
Paytable: 'Gevinsttabel',
|
|
3412
|
+
'Pays anywhere': 'Udbetaler overalt',
|
|
3413
|
+
Price: 'Pris',
|
|
3414
|
+
'Raise bet': 'Hæv indsats',
|
|
3415
|
+
Replay: 'Genspil',
|
|
3416
|
+
RTP: 'RTP',
|
|
3417
|
+
SFX: 'Lydeffekter',
|
|
3418
|
+
Settings: 'Indstillinger',
|
|
3419
|
+
Sound: 'Lyd',
|
|
3420
|
+
Spin: 'Drej',
|
|
3421
|
+
'Spin automatically a set number of times.': 'Spin automatisk et bestemt antal gange.',
|
|
3422
|
+
'Speed up spin animations.': 'Gør spilanimationer hurtigere.',
|
|
3423
|
+
Start: 'Start',
|
|
3424
|
+
'Start a spin at the current bet.': 'Start et spin med den aktuelle indsats.',
|
|
3425
|
+
'Start replay': 'Start genspil',
|
|
3426
|
+
'Total win': 'Samlet gevinst',
|
|
3427
|
+
Turbo: 'Turbo',
|
|
3428
|
+
'Ways to win': 'Vindermuligheder',
|
|
3429
|
+
Win: 'Gevinst',
|
|
3430
|
+
'Winning shapes': 'Vindende former',
|
|
3431
|
+
},
|
|
3432
|
+
de: {
|
|
3433
|
+
DISCLAIMER: 'Haftungsausschluss',
|
|
3434
|
+
Activate: 'Aktivieren',
|
|
3435
|
+
Autoplay: 'Autoplay',
|
|
3436
|
+
Balance: 'Kontostand',
|
|
3437
|
+
Bet: 'Einsatz',
|
|
3438
|
+
'BUY BONUS': 'BONUS KAUFEN',
|
|
3439
|
+
Buy: 'Kaufen',
|
|
3440
|
+
'Buy bonus': 'Bonus kaufen',
|
|
3441
|
+
Cancel: 'Abbrechen',
|
|
3442
|
+
Close: 'Schließen',
|
|
3443
|
+
'Cluster pays': 'Cluster-Gewinne',
|
|
3444
|
+
Confirm: 'Bestätigen',
|
|
3445
|
+
Controls: 'Steuerung',
|
|
3446
|
+
'Decrease your stake.': 'Einsatz verringern.',
|
|
3447
|
+
DISABLE: 'DEAKTIVIEREN',
|
|
3448
|
+
'Dismiss the current overlay.': 'Aktuelles Overlay schließen.',
|
|
3449
|
+
'Free spins': 'Freispiele',
|
|
3450
|
+
Game: 'Spiel',
|
|
3451
|
+
'Game info': 'Spielinfo',
|
|
3452
|
+
Hotkeys: 'Tastenkürzel',
|
|
3453
|
+
'Increase your stake.': 'Einsatz erhöhen.',
|
|
3454
|
+
'Lower bet': 'Einsatz senken',
|
|
3455
|
+
'Master volume': 'Gesamtlautstärke',
|
|
3456
|
+
'Max win': 'Max. Gewinn',
|
|
3457
|
+
Menu: 'Menü',
|
|
3458
|
+
'Menu & info': 'Menü & Info',
|
|
3459
|
+
Modes: 'Modi',
|
|
3460
|
+
Music: 'Musik',
|
|
3461
|
+
Mute: 'Stummschalten',
|
|
3462
|
+
'Mute or unmute the game.': 'Ton stummschalten oder aktivieren.',
|
|
3463
|
+
Navigate: 'Navigieren',
|
|
3464
|
+
'Open settings and game info.': 'Einstellungen und Spielinfo öffnen.',
|
|
3465
|
+
'Open the paytable and rules.': 'Gewinntabelle und Regeln öffnen.',
|
|
3466
|
+
'Pay a fixed cost to enter a bonus feature.': 'Zahle einen festen Betrag, um ein Bonus-Feature zu aktivieren.',
|
|
3467
|
+
Paylines: 'Gewinnlinien',
|
|
3468
|
+
Paytable: 'Gewinntabelle',
|
|
3469
|
+
'Pays anywhere': 'Zahlt überall',
|
|
3470
|
+
Price: 'Preis',
|
|
3471
|
+
'Raise bet': 'Einsatz erhöhen',
|
|
3472
|
+
Replay: 'Wiederholen',
|
|
3473
|
+
RTP: 'RTP',
|
|
3474
|
+
SFX: 'Soundeffekte',
|
|
3475
|
+
Settings: 'Einstellungen',
|
|
3476
|
+
Sound: 'Ton',
|
|
3477
|
+
Spin: 'Drehen',
|
|
3478
|
+
'Spin automatically a set number of times.': 'Automatisch eine festgelegte Anzahl von Runden spielen.',
|
|
3479
|
+
'Speed up spin animations.': 'Animationen beschleunigen.',
|
|
3480
|
+
Start: 'Start',
|
|
3481
|
+
'Start a spin at the current bet.': 'Mit dem aktuellen Einsatz drehen.',
|
|
3482
|
+
'Start replay': 'Wiederholen',
|
|
3483
|
+
'Total win': 'Gesamtgewinn',
|
|
3484
|
+
Turbo: 'Turbo',
|
|
3485
|
+
'Ways to win': 'Gewinnwege',
|
|
3486
|
+
Win: 'Gewinn',
|
|
3487
|
+
'Winning shapes': 'Gewinnmuster',
|
|
3488
|
+
},
|
|
3489
|
+
es: {
|
|
3490
|
+
DISCLAIMER: 'Aviso legal',
|
|
3491
|
+
Activate: 'Activar',
|
|
3492
|
+
Autoplay: 'Giro automático',
|
|
3493
|
+
Balance: 'Saldo',
|
|
3494
|
+
Bet: 'Apuesta',
|
|
3495
|
+
'BUY BONUS': 'COMPRAR BONO',
|
|
3496
|
+
Buy: 'Comprar',
|
|
3497
|
+
'Buy bonus': 'Comprar bono',
|
|
3498
|
+
Cancel: 'Cancelar',
|
|
3499
|
+
Close: 'Cerrar',
|
|
3500
|
+
'Cluster pays': 'Pago en racimo',
|
|
3501
|
+
Confirm: 'Confirmar',
|
|
3502
|
+
Controls: 'Controles',
|
|
3503
|
+
'Decrease your stake.': 'Reducir apuesta.',
|
|
3504
|
+
DISABLE: 'DESACTIVAR',
|
|
3505
|
+
'Dismiss the current overlay.': 'Cerrar el panel actual.',
|
|
3506
|
+
'Free spins': 'Giros gratis',
|
|
3507
|
+
Game: 'Juego',
|
|
3508
|
+
'Game info': 'Info del juego',
|
|
3509
|
+
Hotkeys: 'Atajos de teclado',
|
|
3510
|
+
'Increase your stake.': 'Aumentar apuesta.',
|
|
3511
|
+
'Lower bet': 'Bajar apuesta',
|
|
3512
|
+
'Master volume': 'Volumen principal',
|
|
3513
|
+
'Max win': 'Ganancia máxima',
|
|
3514
|
+
Menu: 'Menú',
|
|
3515
|
+
'Menu & info': 'Menú e info',
|
|
3516
|
+
Modes: 'Modos',
|
|
3517
|
+
Music: 'Música',
|
|
3518
|
+
Mute: 'Silenciar',
|
|
3519
|
+
'Mute or unmute the game.': 'Activar o silenciar el sonido.',
|
|
3520
|
+
Navigate: 'Navegar',
|
|
3521
|
+
'Open settings and game info.': 'Abrir ajustes e info del juego.',
|
|
3522
|
+
'Open the paytable and rules.': 'Abrir tabla de pagos y reglas.',
|
|
3523
|
+
'Pay a fixed cost to enter a bonus feature.': 'Paga un coste fijo para acceder a una función de bono.',
|
|
3524
|
+
Paylines: 'Líneas de pago',
|
|
3525
|
+
Paytable: 'Tabla de pagos',
|
|
3526
|
+
'Pays anywhere': 'Paga en cualquier lugar',
|
|
3527
|
+
Price: 'Precio',
|
|
3528
|
+
'Raise bet': 'Subir apuesta',
|
|
3529
|
+
Replay: 'Repetir',
|
|
3530
|
+
RTP: 'RTP',
|
|
3531
|
+
SFX: 'Efectos de sonido',
|
|
3532
|
+
Settings: 'Ajustes',
|
|
3533
|
+
Sound: 'Sonido',
|
|
3534
|
+
Spin: 'Girar',
|
|
3535
|
+
'Spin automatically a set number of times.': 'Girar automáticamente un número determinado de veces.',
|
|
3536
|
+
'Speed up spin animations.': 'Acelerar las animaciones de giro.',
|
|
3537
|
+
Start: 'Iniciar',
|
|
3538
|
+
'Start a spin at the current bet.': 'Iniciar un giro con la apuesta actual.',
|
|
3539
|
+
'Start replay': 'Iniciar repetición',
|
|
3540
|
+
'Total win': 'Ganancia total',
|
|
3541
|
+
Turbo: 'Turbo',
|
|
3542
|
+
'Ways to win': 'Formas de ganar',
|
|
3543
|
+
Win: 'Premio',
|
|
3544
|
+
'Winning shapes': 'Figuras ganadoras',
|
|
3545
|
+
},
|
|
3546
|
+
fi: {
|
|
3547
|
+
DISCLAIMER: 'Vastuuvapauslauseke',
|
|
3548
|
+
Activate: 'Aktivoi',
|
|
3549
|
+
Autoplay: 'Automaattipeli',
|
|
3550
|
+
Balance: 'Saldo',
|
|
3551
|
+
Bet: 'Panos',
|
|
3552
|
+
'BUY BONUS': 'OSTA BONUS',
|
|
3553
|
+
Buy: 'Osta',
|
|
3554
|
+
'Buy bonus': 'Osta bonus',
|
|
3555
|
+
Cancel: 'Peruuta',
|
|
3556
|
+
Close: 'Sulje',
|
|
3557
|
+
'Cluster pays': 'Ryhmävoitto',
|
|
3558
|
+
Confirm: 'Vahvista',
|
|
3559
|
+
Controls: 'Ohjaimet',
|
|
3560
|
+
'Decrease your stake.': 'Pienennä panostasi.',
|
|
3561
|
+
DISABLE: 'POISTA KÄYTÖSTÄ',
|
|
3562
|
+
'Dismiss the current overlay.': 'Sulje nykyinen näkymä.',
|
|
3563
|
+
'Free spins': 'Ilmaiskierrokset',
|
|
3564
|
+
Game: 'Peli',
|
|
3565
|
+
'Game info': 'Pelitiedot',
|
|
3566
|
+
Hotkeys: 'Pikanäppäimet',
|
|
3567
|
+
'Increase your stake.': 'Kasvata panostasi.',
|
|
3568
|
+
'Lower bet': 'Pienennä panosta',
|
|
3569
|
+
'Master volume': 'Pääänenvoimakkuus',
|
|
3570
|
+
'Max win': 'Maksimivoitto',
|
|
3571
|
+
Menu: 'Valikko',
|
|
3572
|
+
'Menu & info': 'Valikko ja tiedot',
|
|
3573
|
+
Modes: 'Tilat',
|
|
3574
|
+
Music: 'Musiikki',
|
|
3575
|
+
Mute: 'Mykistä',
|
|
3576
|
+
'Mute or unmute the game.': 'Mykistä tai poista mykistys.',
|
|
3577
|
+
Navigate: 'Navigoi',
|
|
3578
|
+
'Open settings and game info.': 'Avaa asetukset ja pelitiedot.',
|
|
3579
|
+
'Open the paytable and rules.': 'Avaa voittotaulukko ja säännöt.',
|
|
3580
|
+
'Pay a fixed cost to enter a bonus feature.': 'Maksa kiinteä summa päästäksesi bonusominaisuuteen.',
|
|
3581
|
+
Paylines: 'Voittolinjat',
|
|
3582
|
+
Paytable: 'Voittotaulukko',
|
|
3583
|
+
'Pays anywhere': 'Voittaa missä tahansa',
|
|
3584
|
+
Price: 'Hinta',
|
|
3585
|
+
'Raise bet': 'Nosta panosta',
|
|
3586
|
+
Replay: 'Toista uudelleen',
|
|
3587
|
+
RTP: 'RTP',
|
|
3588
|
+
SFX: 'Ääniefektit',
|
|
3589
|
+
Settings: 'Asetukset',
|
|
3590
|
+
Sound: 'Ääni',
|
|
3591
|
+
Spin: 'Pyöräytä',
|
|
3592
|
+
'Spin automatically a set number of times.': 'Pyöräytä automaattisesti määritetty määrä kertoja.',
|
|
3593
|
+
'Speed up spin animations.': 'Nopeuta pyöräytysanimaatioita.',
|
|
3594
|
+
Start: 'Aloita',
|
|
3595
|
+
'Start a spin at the current bet.': 'Aloita pyöräytys nykyisellä panoksella.',
|
|
3596
|
+
'Start replay': 'Aloita uudelleentoisto',
|
|
3597
|
+
'Total win': 'Kokonaisvoitto',
|
|
3598
|
+
Turbo: 'Turbo',
|
|
3599
|
+
'Ways to win': 'Voittotavat',
|
|
3600
|
+
Win: 'Voitto',
|
|
3601
|
+
'Winning shapes': 'Voittokuviot',
|
|
3602
|
+
},
|
|
3603
|
+
fr: {
|
|
3604
|
+
DISCLAIMER: 'Avertissement',
|
|
3605
|
+
Activate: 'Activer',
|
|
3606
|
+
Autoplay: 'Jeu automatique',
|
|
3607
|
+
Balance: 'Solde',
|
|
3608
|
+
Bet: 'Mise',
|
|
3609
|
+
'BUY BONUS': 'ACHETER BONUS',
|
|
3610
|
+
Buy: 'Acheter',
|
|
3611
|
+
'Buy bonus': 'Acheter un bonus',
|
|
3612
|
+
Cancel: 'Annuler',
|
|
3613
|
+
Close: 'Fermer',
|
|
3614
|
+
'Cluster pays': 'Gains en grappe',
|
|
3615
|
+
Confirm: 'Confirmer',
|
|
3616
|
+
Controls: 'Commandes',
|
|
3617
|
+
'Decrease your stake.': 'Diminuer votre mise.',
|
|
3618
|
+
DISABLE: 'DÉSACTIVER',
|
|
3619
|
+
'Dismiss the current overlay.': 'Fermer le panneau actuel.',
|
|
3620
|
+
'Free spins': 'Tours gratuits',
|
|
3621
|
+
Game: 'Jeu',
|
|
3622
|
+
'Game info': 'Infos du jeu',
|
|
3623
|
+
Hotkeys: 'Raccourcis clavier',
|
|
3624
|
+
'Increase your stake.': 'Augmenter votre mise.',
|
|
3625
|
+
'Lower bet': 'Baisser la mise',
|
|
3626
|
+
'Master volume': 'Volume principal',
|
|
3627
|
+
'Max win': 'Gain maximum',
|
|
3628
|
+
Menu: 'Menu',
|
|
3629
|
+
'Menu & info': 'Menu et info',
|
|
3630
|
+
Modes: 'Modes',
|
|
3631
|
+
Music: 'Musique',
|
|
3632
|
+
Mute: 'Couper le son',
|
|
3633
|
+
'Mute or unmute the game.': 'Couper ou rétablir le son.',
|
|
3634
|
+
Navigate: 'Naviguer',
|
|
3635
|
+
'Open settings and game info.': 'Ouvrir les paramètres et infos du jeu.',
|
|
3636
|
+
'Open the paytable and rules.': 'Ouvrir la table des gains et les règles.',
|
|
3637
|
+
'Pay a fixed cost to enter a bonus feature.': 'Payez un coût fixe pour accéder à une fonctionnalité bonus.',
|
|
3638
|
+
Paylines: 'Lignes de paiement',
|
|
3639
|
+
Paytable: 'Table des gains',
|
|
3640
|
+
'Pays anywhere': 'Gains sur toute la grille',
|
|
3641
|
+
Price: 'Prix',
|
|
3642
|
+
'Raise bet': 'Augmenter la mise',
|
|
3643
|
+
Replay: 'Revoir',
|
|
3644
|
+
RTP: 'RTP',
|
|
3645
|
+
SFX: 'Effets sonores',
|
|
3646
|
+
Settings: 'Paramètres',
|
|
3647
|
+
Sound: 'Son',
|
|
3648
|
+
Spin: 'Tourner',
|
|
3649
|
+
'Spin automatically a set number of times.': 'Tourner automatiquement un nombre de fois défini.',
|
|
3650
|
+
'Speed up spin animations.': 'Accélérer les animations de spin.',
|
|
3651
|
+
Start: 'Lancer',
|
|
3652
|
+
'Start a spin at the current bet.': 'Lancer un tour avec la mise actuelle.',
|
|
3653
|
+
'Start replay': 'Lancer le replay',
|
|
3654
|
+
'Total win': 'Gain total',
|
|
3655
|
+
Turbo: 'Turbo',
|
|
3656
|
+
'Ways to win': 'Façons de gagner',
|
|
3657
|
+
Win: 'Gain',
|
|
3658
|
+
'Winning shapes': 'Figures gagnantes',
|
|
3659
|
+
},
|
|
3660
|
+
hi: {
|
|
3661
|
+
DISCLAIMER: 'अस्वीकरण',
|
|
3662
|
+
Activate: 'सक्रिय करें',
|
|
3663
|
+
Autoplay: 'ऑटोप्ले',
|
|
3664
|
+
Balance: 'बैलेंस',
|
|
3665
|
+
Bet: 'दांव',
|
|
3666
|
+
'BUY BONUS': 'बोनस खरीदें',
|
|
3667
|
+
Buy: 'खरीदें',
|
|
3668
|
+
'Buy bonus': 'बोनस खरीदें',
|
|
3669
|
+
Cancel: 'रद्द करें',
|
|
3670
|
+
Close: 'बंद करें',
|
|
3671
|
+
'Cluster pays': 'क्लस्टर भुगतान',
|
|
3672
|
+
Confirm: 'पुष्टि करें',
|
|
3673
|
+
Controls: 'नियंत्रण',
|
|
3674
|
+
'Decrease your stake.': 'अपना दांव कम करें।',
|
|
3675
|
+
DISABLE: 'बंद करें',
|
|
3676
|
+
'Dismiss the current overlay.': 'वर्तमान ओवरले बंद करें।',
|
|
3677
|
+
'Free spins': 'फ्री स्पिन',
|
|
3678
|
+
Game: 'खेल',
|
|
3679
|
+
'Game info': 'गेम जानकारी',
|
|
3680
|
+
Hotkeys: 'कीबोर्ड शॉर्टकट',
|
|
3681
|
+
'Increase your stake.': 'अपना दांव बढ़ाएं।',
|
|
3682
|
+
'Lower bet': 'दांव घटाएं',
|
|
3683
|
+
'Master volume': 'मुख्य वॉल्यूम',
|
|
3684
|
+
'Max win': 'अधिकतम जीत',
|
|
3685
|
+
Menu: 'मेनू',
|
|
3686
|
+
'Menu & info': 'मेनू और जानकारी',
|
|
3687
|
+
Modes: 'मोड',
|
|
3688
|
+
Music: 'संगीत',
|
|
3689
|
+
Mute: 'म्यूट करें',
|
|
3690
|
+
'Mute or unmute the game.': 'गेम को म्यूट या अनम्यूट करें।',
|
|
3691
|
+
Navigate: 'नेविगेट करें',
|
|
3692
|
+
'Open settings and game info.': 'सेटिंग और गेम जानकारी खोलें।',
|
|
3693
|
+
'Open the paytable and rules.': 'पेटेबल और नियम खोलें।',
|
|
3694
|
+
'Pay a fixed cost to enter a bonus feature.': 'बोनस फीचर में प्रवेश के लिए एक निश्चित राशि दें।',
|
|
3695
|
+
Paylines: 'पेलाइन',
|
|
3696
|
+
Paytable: 'पेटेबल',
|
|
3697
|
+
'Pays anywhere': 'कहीं भी जीत',
|
|
3698
|
+
Price: 'मूल्य',
|
|
3699
|
+
'Raise bet': 'दांव बढ़ाएं',
|
|
3700
|
+
Replay: 'दोबारा खेलें',
|
|
3701
|
+
RTP: 'RTP',
|
|
3702
|
+
SFX: 'ध्वनि प्रभाव',
|
|
3703
|
+
Settings: 'सेटिंग',
|
|
3704
|
+
Sound: 'ध्वनि',
|
|
3705
|
+
Spin: 'स्पिन',
|
|
3706
|
+
'Spin automatically a set number of times.': 'एक निश्चित संख्या में स्वचालित रूप से स्पिन करें।',
|
|
3707
|
+
'Speed up spin animations.': 'स्पिन एनिमेशन को तेज़ करें।',
|
|
3708
|
+
Start: 'शुरू करें',
|
|
3709
|
+
'Start a spin at the current bet.': 'वर्तमान दांव पर स्पिन शुरू करें।',
|
|
3710
|
+
'Start replay': 'रीप्ले शुरू करें',
|
|
3711
|
+
'Total win': 'कुल जीत',
|
|
3712
|
+
Turbo: 'टर्बो',
|
|
3713
|
+
'Ways to win': 'जीत के तरीके',
|
|
3714
|
+
Win: 'जीत',
|
|
3715
|
+
'Winning shapes': 'जीत के आकार',
|
|
3716
|
+
},
|
|
3717
|
+
id: {
|
|
3718
|
+
DISCLAIMER: 'Penafian',
|
|
3719
|
+
Activate: 'Aktifkan',
|
|
3720
|
+
Autoplay: 'Putar Otomatis',
|
|
3721
|
+
Balance: 'Saldo',
|
|
3722
|
+
Bet: 'Taruhan',
|
|
3723
|
+
'BUY BONUS': 'BELI BONUS',
|
|
3724
|
+
Buy: 'Beli',
|
|
3725
|
+
'Buy bonus': 'Beli bonus',
|
|
3726
|
+
Cancel: 'Batal',
|
|
3727
|
+
Close: 'Tutup',
|
|
3728
|
+
'Cluster pays': 'Bayar kluster',
|
|
3729
|
+
Confirm: 'Konfirmasi',
|
|
3730
|
+
Controls: 'Kontrol',
|
|
3731
|
+
'Decrease your stake.': 'Kurangi taruhan Anda.',
|
|
3732
|
+
DISABLE: 'NONAKTIFKAN',
|
|
3733
|
+
'Dismiss the current overlay.': 'Tutup overlay saat ini.',
|
|
3734
|
+
'Free spins': 'Putaran gratis',
|
|
3735
|
+
Game: 'Permainan',
|
|
3736
|
+
'Game info': 'Info permainan',
|
|
3737
|
+
Hotkeys: 'Pintasan keyboard',
|
|
3738
|
+
'Increase your stake.': 'Tingkatkan taruhan Anda.',
|
|
3739
|
+
'Lower bet': 'Turunkan taruhan',
|
|
3740
|
+
'Master volume': 'Volume utama',
|
|
3741
|
+
'Max win': 'Kemenangan maks',
|
|
3742
|
+
Menu: 'Menu',
|
|
3743
|
+
'Menu & info': 'Menu & info',
|
|
3744
|
+
Modes: 'Mode',
|
|
3745
|
+
Music: 'Musik',
|
|
3746
|
+
Mute: 'Bisukan',
|
|
3747
|
+
'Mute or unmute the game.': 'Bisukan atau aktifkan suara permainan.',
|
|
3748
|
+
Navigate: 'Navigasi',
|
|
3749
|
+
'Open settings and game info.': 'Buka pengaturan dan info permainan.',
|
|
3750
|
+
'Open the paytable and rules.': 'Buka tabel pembayaran dan aturan.',
|
|
3751
|
+
'Pay a fixed cost to enter a bonus feature.': 'Bayar biaya tetap untuk masuk ke fitur bonus.',
|
|
3752
|
+
Paylines: 'Garis pembayaran',
|
|
3753
|
+
Paytable: 'Tabel pembayaran',
|
|
3754
|
+
'Pays anywhere': 'Menang di mana saja',
|
|
3755
|
+
Price: 'Harga',
|
|
3756
|
+
'Raise bet': 'Naikkan taruhan',
|
|
3757
|
+
Replay: 'Putar ulang',
|
|
3758
|
+
RTP: 'RTP',
|
|
3759
|
+
SFX: 'Efek suara',
|
|
3760
|
+
Settings: 'Pengaturan',
|
|
3761
|
+
Sound: 'Suara',
|
|
3762
|
+
Spin: 'Putar',
|
|
3763
|
+
'Spin automatically a set number of times.': 'Putar otomatis sejumlah kali yang ditentukan.',
|
|
3764
|
+
'Speed up spin animations.': 'Percepat animasi putaran.',
|
|
3765
|
+
Start: 'Mulai',
|
|
3766
|
+
'Start a spin at the current bet.': 'Mulai putaran dengan taruhan saat ini.',
|
|
3767
|
+
'Start replay': 'Mulai putar ulang',
|
|
3768
|
+
'Total win': 'Total kemenangan',
|
|
3769
|
+
Turbo: 'Turbo',
|
|
3770
|
+
'Ways to win': 'Cara menang',
|
|
3771
|
+
Win: 'Menang',
|
|
3772
|
+
'Winning shapes': 'Bentuk kemenangan',
|
|
3773
|
+
},
|
|
3774
|
+
ja: {
|
|
3775
|
+
DISCLAIMER: '免責事項',
|
|
3776
|
+
Activate: '有効化',
|
|
3777
|
+
Autoplay: 'オートプレイ',
|
|
3778
|
+
Balance: '残高',
|
|
3779
|
+
Bet: 'ベット',
|
|
3780
|
+
'BUY BONUS': 'ボーナス購入',
|
|
3781
|
+
Buy: '購入',
|
|
3782
|
+
'Buy bonus': 'ボーナス購入',
|
|
3783
|
+
Cancel: 'キャンセル',
|
|
3784
|
+
Close: '閉じる',
|
|
3785
|
+
'Cluster pays': 'クラスター配当',
|
|
3786
|
+
Confirm: '確認',
|
|
3787
|
+
Controls: '操作方法',
|
|
3788
|
+
'Decrease your stake.': 'ベットを減らす。',
|
|
3789
|
+
DISABLE: '無効',
|
|
3790
|
+
'Dismiss the current overlay.': '現在のオーバーレイを閉じる。',
|
|
3791
|
+
'Free spins': 'フリースピン',
|
|
3792
|
+
Game: 'ゲーム',
|
|
3793
|
+
'Game info': 'ゲーム情報',
|
|
3794
|
+
Hotkeys: 'キーボードショートカット',
|
|
3795
|
+
'Increase your stake.': 'ベットを増やす。',
|
|
3796
|
+
'Lower bet': 'ベットを下げる',
|
|
3797
|
+
'Master volume': 'マスターボリューム',
|
|
3798
|
+
'Max win': '最大当選',
|
|
3799
|
+
Menu: 'メニュー',
|
|
3800
|
+
'Menu & info': 'メニューと情報',
|
|
3801
|
+
Modes: 'モード',
|
|
3802
|
+
Music: 'ミュージック',
|
|
3803
|
+
Mute: 'ミュート',
|
|
3804
|
+
'Mute or unmute the game.': 'ゲームをミュート/ミュート解除する。',
|
|
3805
|
+
Navigate: 'ナビゲート',
|
|
3806
|
+
'Open settings and game info.': '設定とゲーム情報を開く。',
|
|
3807
|
+
'Open the paytable and rules.': '配当表とルールを開く。',
|
|
3808
|
+
'Pay a fixed cost to enter a bonus feature.': 'ボーナス機能に入るために固定料金を支払う。',
|
|
3809
|
+
Paylines: 'ペイライン',
|
|
3810
|
+
Paytable: '配当表',
|
|
3811
|
+
'Pays anywhere': 'どこでも当選',
|
|
3812
|
+
Price: '価格',
|
|
3813
|
+
'Raise bet': 'ベットを上げる',
|
|
3814
|
+
Replay: 'リプレイ',
|
|
3815
|
+
RTP: 'RTP',
|
|
3816
|
+
SFX: '効果音',
|
|
3817
|
+
Settings: '設定',
|
|
3818
|
+
Sound: 'サウンド',
|
|
3819
|
+
Spin: 'スピン',
|
|
3820
|
+
'Spin automatically a set number of times.': '設定した回数だけ自動でスピンする。',
|
|
3821
|
+
'Speed up spin animations.': 'スピンアニメーションを高速化する。',
|
|
3822
|
+
Start: 'スタート',
|
|
3823
|
+
'Start a spin at the current bet.': '現在のベットでスピンを開始する。',
|
|
3824
|
+
'Start replay': 'リプレイ開始',
|
|
3825
|
+
'Total win': '合計当選',
|
|
3826
|
+
Turbo: 'ターボ',
|
|
3827
|
+
'Ways to win': '当選方法',
|
|
3828
|
+
Win: '当選',
|
|
3829
|
+
'Winning shapes': '当選形状',
|
|
3830
|
+
},
|
|
3831
|
+
ko: {
|
|
3832
|
+
DISCLAIMER: '면책 조항',
|
|
3833
|
+
Activate: '활성화',
|
|
3834
|
+
Autoplay: '자동 플레이',
|
|
3835
|
+
Balance: '잔액',
|
|
3836
|
+
Bet: '베팅',
|
|
3837
|
+
'BUY BONUS': '보너스 구매',
|
|
3838
|
+
Buy: '구매',
|
|
3839
|
+
'Buy bonus': '보너스 구매',
|
|
3840
|
+
Cancel: '취소',
|
|
3841
|
+
Close: '닫기',
|
|
3842
|
+
'Cluster pays': '클러스터 페이',
|
|
3843
|
+
Confirm: '확인',
|
|
3844
|
+
Controls: '조작법',
|
|
3845
|
+
'Decrease your stake.': '베팅을 줄이세요.',
|
|
3846
|
+
DISABLE: '비활성화',
|
|
3847
|
+
'Dismiss the current overlay.': '현재 오버레이를 닫습니다.',
|
|
3848
|
+
'Free spins': '무료 스핀',
|
|
3849
|
+
Game: '게임',
|
|
3850
|
+
'Game info': '게임 정보',
|
|
3851
|
+
Hotkeys: '키보드 단축키',
|
|
3852
|
+
'Increase your stake.': '베팅을 늘리세요.',
|
|
3853
|
+
'Lower bet': '베팅 낮추기',
|
|
3854
|
+
'Master volume': '마스터 볼륨',
|
|
3855
|
+
'Max win': '최대 당첨',
|
|
3856
|
+
Menu: '메뉴',
|
|
3857
|
+
'Menu & info': '메뉴 & 정보',
|
|
3858
|
+
Modes: '모드',
|
|
3859
|
+
Music: '음악',
|
|
3860
|
+
Mute: '음소거',
|
|
3861
|
+
'Mute or unmute the game.': '게임 소리를 켜거나 끕니다.',
|
|
3862
|
+
Navigate: '이동',
|
|
3863
|
+
'Open settings and game info.': '설정 및 게임 정보를 엽니다.',
|
|
3864
|
+
'Open the paytable and rules.': '페이테이블 및 규칙을 엽니다.',
|
|
3865
|
+
'Pay a fixed cost to enter a bonus feature.': '고정 비용을 지불하고 보너스 기능에 진입하세요.',
|
|
3866
|
+
Paylines: '페이라인',
|
|
3867
|
+
Paytable: '페이테이블',
|
|
3868
|
+
'Pays anywhere': '어디서나 당첨',
|
|
3869
|
+
Price: '가격',
|
|
3870
|
+
'Raise bet': '베팅 올리기',
|
|
3871
|
+
Replay: '다시보기',
|
|
3872
|
+
RTP: 'RTP',
|
|
3873
|
+
SFX: '효과음',
|
|
3874
|
+
Settings: '설정',
|
|
3875
|
+
Sound: '사운드',
|
|
3876
|
+
Spin: '스핀',
|
|
3877
|
+
'Spin automatically a set number of times.': '정해진 횟수만큼 자동으로 스핀합니다.',
|
|
3878
|
+
'Speed up spin animations.': '스핀 애니메이션을 빠르게 합니다.',
|
|
3879
|
+
Start: '시작',
|
|
3880
|
+
'Start a spin at the current bet.': '현재 베팅으로 스핀을 시작합니다.',
|
|
3881
|
+
'Start replay': '다시보기 시작',
|
|
3882
|
+
'Total win': '총 당첨',
|
|
3883
|
+
Turbo: '터보',
|
|
3884
|
+
'Ways to win': '당첨 방법',
|
|
3885
|
+
Win: '당첨',
|
|
3886
|
+
'Winning shapes': '당첨 패턴',
|
|
3887
|
+
},
|
|
3888
|
+
pl: {
|
|
3889
|
+
DISCLAIMER: 'Zastrzeżenie',
|
|
3890
|
+
Activate: 'Aktywuj',
|
|
3891
|
+
Autoplay: 'Autoplay',
|
|
3892
|
+
Balance: 'Saldo',
|
|
3893
|
+
Bet: 'Zakład',
|
|
3894
|
+
'BUY BONUS': 'KUP BONUS',
|
|
3895
|
+
Buy: 'Kup',
|
|
3896
|
+
'Buy bonus': 'Kup bonus',
|
|
3897
|
+
Cancel: 'Anuluj',
|
|
3898
|
+
Close: 'Zamknij',
|
|
3899
|
+
'Cluster pays': 'Wypłaty klastrowe',
|
|
3900
|
+
Confirm: 'Potwierdź',
|
|
3901
|
+
Controls: 'Sterowanie',
|
|
3902
|
+
'Decrease your stake.': 'Zmniejsz swój zakład.',
|
|
3903
|
+
DISABLE: 'WYŁĄCZ',
|
|
3904
|
+
'Dismiss the current overlay.': 'Zamknij bieżące okno.',
|
|
3905
|
+
'Free spins': 'Darmowe spiny',
|
|
3906
|
+
Game: 'Gra',
|
|
3907
|
+
'Game info': 'Informacje o grze',
|
|
3908
|
+
Hotkeys: 'Skróty klawiaturowe',
|
|
3909
|
+
'Increase your stake.': 'Zwiększ swój zakład.',
|
|
3910
|
+
'Lower bet': 'Obniż zakład',
|
|
3911
|
+
'Master volume': 'Głośność główna',
|
|
3912
|
+
'Max win': 'Maks. wygrana',
|
|
3913
|
+
Menu: 'Menu',
|
|
3914
|
+
'Menu & info': 'Menu i informacje',
|
|
3915
|
+
Modes: 'Tryby',
|
|
3916
|
+
Music: 'Muzyka',
|
|
3917
|
+
Mute: 'Wycisz',
|
|
3918
|
+
'Mute or unmute the game.': 'Wycisz lub odcisz dźwięk gry.',
|
|
3919
|
+
Navigate: 'Nawiguj',
|
|
3920
|
+
'Open settings and game info.': 'Otwórz ustawienia i informacje o grze.',
|
|
3921
|
+
'Open the paytable and rules.': 'Otwórz tabelę wygranych i zasady.',
|
|
3922
|
+
'Pay a fixed cost to enter a bonus feature.': 'Zapłać stałą kwotę, aby wejść do funkcji bonusowej.',
|
|
3923
|
+
Paylines: 'Linie wygrywające',
|
|
3924
|
+
Paytable: 'Tabela wygranych',
|
|
3925
|
+
'Pays anywhere': 'Wypłaca wszędzie',
|
|
3926
|
+
Price: 'Cena',
|
|
3927
|
+
'Raise bet': 'Podnieś zakład',
|
|
3928
|
+
Replay: 'Odtwórz ponownie',
|
|
3929
|
+
RTP: 'RTP',
|
|
3930
|
+
SFX: 'Efekty dźwiękowe',
|
|
3931
|
+
Settings: 'Ustawienia',
|
|
3932
|
+
Sound: 'Dźwięk',
|
|
3933
|
+
Spin: 'Zakręć',
|
|
3934
|
+
'Spin automatically a set number of times.': 'Obracaj automatycznie określoną liczbę razy.',
|
|
3935
|
+
'Speed up spin animations.': 'Przyspiesz animacje obrotów.',
|
|
3936
|
+
Start: 'Start',
|
|
3937
|
+
'Start a spin at the current bet.': 'Rozpocznij obrót przy bieżącym zakładzie.',
|
|
3938
|
+
'Start replay': 'Rozpocznij odtwarzanie',
|
|
3939
|
+
'Total win': 'Łączna wygrana',
|
|
3940
|
+
Turbo: 'Turbo',
|
|
3941
|
+
'Ways to win': 'Sposoby wygrywania',
|
|
3942
|
+
Win: 'Wygrana',
|
|
3943
|
+
'Winning shapes': 'Wzory wygrywające',
|
|
3944
|
+
},
|
|
3945
|
+
pt: {
|
|
3946
|
+
DISCLAIMER: 'Aviso legal',
|
|
3947
|
+
Activate: 'Ativar',
|
|
3948
|
+
Autoplay: 'Giro automático',
|
|
3949
|
+
Balance: 'Saldo',
|
|
3950
|
+
Bet: 'Aposta',
|
|
3951
|
+
'BUY BONUS': 'COMPRAR BÔNUS',
|
|
3952
|
+
Buy: 'Comprar',
|
|
3953
|
+
'Buy bonus': 'Comprar bônus',
|
|
3954
|
+
Cancel: 'Cancelar',
|
|
3955
|
+
Close: 'Fechar',
|
|
3956
|
+
'Cluster pays': 'Pagamento em cluster',
|
|
3957
|
+
Confirm: 'Confirmar',
|
|
3958
|
+
Controls: 'Controles',
|
|
3959
|
+
'Decrease your stake.': 'Diminuir aposta.',
|
|
3960
|
+
DISABLE: 'DESATIVAR',
|
|
3961
|
+
'Dismiss the current overlay.': 'Fechar o painel atual.',
|
|
3962
|
+
'Free spins': 'Giros grátis',
|
|
3963
|
+
Game: 'Jogo',
|
|
3964
|
+
'Game info': 'Info do jogo',
|
|
3965
|
+
Hotkeys: 'Atalhos de teclado',
|
|
3966
|
+
'Increase your stake.': 'Aumentar aposta.',
|
|
3967
|
+
'Lower bet': 'Baixar aposta',
|
|
3968
|
+
'Master volume': 'Volume principal',
|
|
3969
|
+
'Max win': 'Ganho máximo',
|
|
3970
|
+
Menu: 'Menu',
|
|
3971
|
+
'Menu & info': 'Menu e info',
|
|
3972
|
+
Modes: 'Modos',
|
|
3973
|
+
Music: 'Música',
|
|
3974
|
+
Mute: 'Silenciar',
|
|
3975
|
+
'Mute or unmute the game.': 'Ativar ou silenciar o som do jogo.',
|
|
3976
|
+
Navigate: 'Navegar',
|
|
3977
|
+
'Open settings and game info.': 'Abrir configurações e info do jogo.',
|
|
3978
|
+
'Open the paytable and rules.': 'Abrir tabela de pagamentos e regras.',
|
|
3979
|
+
'Pay a fixed cost to enter a bonus feature.': 'Pague um custo fixo para entrar numa funcionalidade de bônus.',
|
|
3980
|
+
Paylines: 'Linhas de pagamento',
|
|
3981
|
+
Paytable: 'Tabela de pagamentos',
|
|
3982
|
+
'Pays anywhere': 'Paga em qualquer posição',
|
|
3983
|
+
Price: 'Preço',
|
|
3984
|
+
'Raise bet': 'Aumentar aposta',
|
|
3985
|
+
Replay: 'Repetir',
|
|
3986
|
+
RTP: 'RTP',
|
|
3987
|
+
SFX: 'Efeitos sonoros',
|
|
3988
|
+
Settings: 'Configurações',
|
|
3989
|
+
Sound: 'Som',
|
|
3990
|
+
Spin: 'Girar',
|
|
3991
|
+
'Spin automatically a set number of times.': 'Girar automaticamente um número definido de vezes.',
|
|
3992
|
+
'Speed up spin animations.': 'Acelerar as animações de giro.',
|
|
3993
|
+
Start: 'Iniciar',
|
|
3994
|
+
'Start a spin at the current bet.': 'Iniciar um giro com a aposta atual.',
|
|
3995
|
+
'Start replay': 'Iniciar repetição',
|
|
3996
|
+
'Total win': 'Ganho total',
|
|
3997
|
+
Turbo: 'Turbo',
|
|
3998
|
+
'Ways to win': 'Formas de ganhar',
|
|
3999
|
+
Win: 'Ganho',
|
|
4000
|
+
'Winning shapes': 'Figuras vencedoras',
|
|
4001
|
+
},
|
|
4002
|
+
ru: {
|
|
4003
|
+
DISCLAIMER: 'Отказ от ответственности',
|
|
4004
|
+
Activate: 'Активировать',
|
|
4005
|
+
Autoplay: 'Автоигра',
|
|
4006
|
+
Balance: 'Баланс',
|
|
4007
|
+
Bet: 'Ставка',
|
|
4008
|
+
'BUY BONUS': 'КУПИТЬ БОНУС',
|
|
4009
|
+
Buy: 'Купить',
|
|
4010
|
+
'Buy bonus': 'Купить бонус',
|
|
4011
|
+
Cancel: 'Отмена',
|
|
4012
|
+
Close: 'Закрыть',
|
|
4013
|
+
'Cluster pays': 'Кластерные выплаты',
|
|
4014
|
+
Confirm: 'Подтвердить',
|
|
4015
|
+
Controls: 'Управление',
|
|
4016
|
+
'Decrease your stake.': 'Уменьшить ставку.',
|
|
4017
|
+
DISABLE: 'ОТКЛЮЧИТЬ',
|
|
4018
|
+
'Dismiss the current overlay.': 'Закрыть текущее окно.',
|
|
4019
|
+
'Free spins': 'Бесплатные вращения',
|
|
4020
|
+
Game: 'Игра',
|
|
4021
|
+
'Game info': 'Информация об игре',
|
|
4022
|
+
Hotkeys: 'Горячие клавиши',
|
|
4023
|
+
'Increase your stake.': 'Увеличить ставку.',
|
|
4024
|
+
'Lower bet': 'Уменьшить ставку',
|
|
4025
|
+
'Master volume': 'Общая громкость',
|
|
4026
|
+
'Max win': 'Макс. выигрыш',
|
|
4027
|
+
Menu: 'Меню',
|
|
4028
|
+
'Menu & info': 'Меню и информация',
|
|
4029
|
+
Modes: 'Режимы',
|
|
4030
|
+
Music: 'Музыка',
|
|
4031
|
+
Mute: 'Отключить звук',
|
|
4032
|
+
'Mute or unmute the game.': 'Включить или отключить звук.',
|
|
4033
|
+
Navigate: 'Навигация',
|
|
4034
|
+
'Open settings and game info.': 'Открыть настройки и информацию об игре.',
|
|
4035
|
+
'Open the paytable and rules.': 'Открыть таблицу выплат и правила.',
|
|
4036
|
+
'Pay a fixed cost to enter a bonus feature.': 'Заплатите фиксированную сумму для входа в бонусный раунд.',
|
|
4037
|
+
Paylines: 'Линии выплат',
|
|
4038
|
+
Paytable: 'Таблица выплат',
|
|
4039
|
+
'Pays anywhere': 'Выплачивает в любом месте',
|
|
4040
|
+
Price: 'Цена',
|
|
4041
|
+
'Raise bet': 'Повысить ставку',
|
|
4042
|
+
Replay: 'Повтор',
|
|
4043
|
+
RTP: 'RTP',
|
|
4044
|
+
SFX: 'Звуковые эффекты',
|
|
4045
|
+
Settings: 'Настройки',
|
|
4046
|
+
Sound: 'Звук',
|
|
4047
|
+
Spin: 'Вращение',
|
|
4048
|
+
'Spin automatically a set number of times.': 'Автоматически вращать заданное количество раз.',
|
|
4049
|
+
'Speed up spin animations.': 'Ускорить анимацию вращений.',
|
|
4050
|
+
Start: 'Начать',
|
|
4051
|
+
'Start a spin at the current bet.': 'Начать вращение с текущей ставкой.',
|
|
4052
|
+
'Start replay': 'Начать повтор',
|
|
4053
|
+
'Total win': 'Общий выигрыш',
|
|
4054
|
+
Turbo: 'Турбо',
|
|
4055
|
+
'Ways to win': 'Способы выигрыша',
|
|
4056
|
+
Win: 'Выигрыш',
|
|
4057
|
+
'Winning shapes': 'Выигрышные комбинации',
|
|
4058
|
+
},
|
|
4059
|
+
tr: {
|
|
4060
|
+
DISCLAIMER: 'Yasal uyarı',
|
|
4061
|
+
Activate: 'Etkinleştir',
|
|
4062
|
+
Autoplay: 'Otomatik Oyun',
|
|
4063
|
+
Balance: 'Bakiye',
|
|
4064
|
+
Bet: 'Bahis',
|
|
4065
|
+
'BUY BONUS': 'BONUS SATIN AL',
|
|
4066
|
+
Buy: 'Satın al',
|
|
4067
|
+
'Buy bonus': 'Bonus satın al',
|
|
4068
|
+
Cancel: 'İptal',
|
|
4069
|
+
Close: 'Kapat',
|
|
4070
|
+
'Cluster pays': 'Küme ödemeleri',
|
|
4071
|
+
Confirm: 'Onayla',
|
|
4072
|
+
Controls: 'Kontroller',
|
|
4073
|
+
'Decrease your stake.': 'Bahsinizi azaltın.',
|
|
4074
|
+
DISABLE: 'DEVRE DIŞI',
|
|
4075
|
+
'Dismiss the current overlay.': 'Mevcut pencereyi kapat.',
|
|
4076
|
+
'Free spins': 'Ücretsiz dönüşler',
|
|
4077
|
+
Game: 'Oyun',
|
|
4078
|
+
'Game info': 'Oyun bilgisi',
|
|
4079
|
+
Hotkeys: 'Klavye kısayolları',
|
|
4080
|
+
'Increase your stake.': 'Bahsinizi artırın.',
|
|
4081
|
+
'Lower bet': 'Bahsi düşür',
|
|
4082
|
+
'Master volume': 'Ana ses',
|
|
4083
|
+
'Max win': 'Maks kazanç',
|
|
4084
|
+
Menu: 'Menü',
|
|
4085
|
+
'Menu & info': 'Menü ve bilgi',
|
|
4086
|
+
Modes: 'Modlar',
|
|
4087
|
+
Music: 'Müzik',
|
|
4088
|
+
Mute: 'Sessiz',
|
|
4089
|
+
'Mute or unmute the game.': 'Oyun sesini aç ya da kapat.',
|
|
4090
|
+
Navigate: 'Gezin',
|
|
4091
|
+
'Open settings and game info.': 'Ayarları ve oyun bilgisini aç.',
|
|
4092
|
+
'Open the paytable and rules.': 'Ödeme tablosunu ve kuralları aç.',
|
|
4093
|
+
'Pay a fixed cost to enter a bonus feature.': 'Bonus özelliğine girmek için sabit bir ücret ödeyin.',
|
|
4094
|
+
Paylines: 'Ödeme çizgileri',
|
|
4095
|
+
Paytable: 'Ödeme tablosu',
|
|
4096
|
+
'Pays anywhere': 'Her yerde kazandırır',
|
|
4097
|
+
Price: 'Fiyat',
|
|
4098
|
+
'Raise bet': 'Bahsi artır',
|
|
4099
|
+
Replay: 'Tekrar oynat',
|
|
4100
|
+
RTP: 'RTP',
|
|
4101
|
+
SFX: 'Ses efektleri',
|
|
4102
|
+
Settings: 'Ayarlar',
|
|
4103
|
+
Sound: 'Ses',
|
|
4104
|
+
Spin: 'Döndür',
|
|
4105
|
+
'Spin automatically a set number of times.': 'Belirlenen sayıda otomatik döndür.',
|
|
4106
|
+
'Speed up spin animations.': 'Döndürme animasyonlarını hızlandır.',
|
|
4107
|
+
Start: 'Başlat',
|
|
4108
|
+
'Start a spin at the current bet.': 'Mevcut bahisle döndürmeyi başlat.',
|
|
4109
|
+
'Start replay': 'Tekrarı başlat',
|
|
4110
|
+
'Total win': 'Toplam kazanç',
|
|
4111
|
+
Turbo: 'Turbo',
|
|
4112
|
+
'Ways to win': 'Kazanma yolları',
|
|
4113
|
+
Win: 'Kazanç',
|
|
4114
|
+
'Winning shapes': 'Kazanan şekiller',
|
|
4115
|
+
},
|
|
4116
|
+
vi: {
|
|
4117
|
+
DISCLAIMER: 'Tuyên bố miễn trừ trách nhiệm',
|
|
4118
|
+
Activate: 'Kích hoạt',
|
|
4119
|
+
Autoplay: 'Tự động quay',
|
|
4120
|
+
Balance: 'Số dư',
|
|
4121
|
+
Bet: 'Cược',
|
|
4122
|
+
'BUY BONUS': 'MUA THƯỞNG',
|
|
4123
|
+
Buy: 'Mua',
|
|
4124
|
+
'Buy bonus': 'Mua thưởng',
|
|
4125
|
+
Cancel: 'Hủy',
|
|
4126
|
+
Close: 'Đóng',
|
|
4127
|
+
'Cluster pays': 'Trả theo cụm',
|
|
4128
|
+
Confirm: 'Xác nhận',
|
|
4129
|
+
Controls: 'Điều khiển',
|
|
4130
|
+
'Decrease your stake.': 'Giảm mức cược.',
|
|
4131
|
+
DISABLE: 'TẮT',
|
|
4132
|
+
'Dismiss the current overlay.': 'Đóng lớp phủ hiện tại.',
|
|
4133
|
+
'Free spins': 'Quay miễn phí',
|
|
4134
|
+
Game: 'Trò chơi',
|
|
4135
|
+
'Game info': 'Thông tin trò chơi',
|
|
4136
|
+
Hotkeys: 'Phím tắt',
|
|
4137
|
+
'Increase your stake.': 'Tăng mức cược.',
|
|
4138
|
+
'Lower bet': 'Giảm cược',
|
|
4139
|
+
'Master volume': 'Âm lượng chính',
|
|
4140
|
+
'Max win': 'Thắng tối đa',
|
|
4141
|
+
Menu: 'Menu',
|
|
4142
|
+
'Menu & info': 'Menu & thông tin',
|
|
4143
|
+
Modes: 'Chế độ',
|
|
4144
|
+
Music: 'Âm nhạc',
|
|
4145
|
+
Mute: 'Tắt âm',
|
|
4146
|
+
'Mute or unmute the game.': 'Tắt hoặc bật âm thanh trò chơi.',
|
|
4147
|
+
Navigate: 'Điều hướng',
|
|
4148
|
+
'Open settings and game info.': 'Mở cài đặt và thông tin trò chơi.',
|
|
4149
|
+
'Open the paytable and rules.': 'Mở bảng trả thưởng và quy tắc.',
|
|
4150
|
+
'Pay a fixed cost to enter a bonus feature.': 'Trả một khoản phí cố định để tham gia tính năng thưởng.',
|
|
4151
|
+
Paylines: 'Đường thắng',
|
|
4152
|
+
Paytable: 'Bảng trả thưởng',
|
|
4153
|
+
'Pays anywhere': 'Trả bất cứ đâu',
|
|
4154
|
+
Price: 'Giá',
|
|
4155
|
+
'Raise bet': 'Tăng cược',
|
|
4156
|
+
Replay: 'Xem lại',
|
|
4157
|
+
RTP: 'RTP',
|
|
4158
|
+
SFX: 'Hiệu ứng âm thanh',
|
|
4159
|
+
Settings: 'Cài đặt',
|
|
4160
|
+
Sound: 'Âm thanh',
|
|
4161
|
+
Spin: 'Quay',
|
|
4162
|
+
'Spin automatically a set number of times.': 'Tự động quay một số lần nhất định.',
|
|
4163
|
+
'Speed up spin animations.': 'Tăng tốc độ hoạt ảnh quay.',
|
|
4164
|
+
Start: 'Bắt đầu',
|
|
4165
|
+
'Start a spin at the current bet.': 'Bắt đầu quay với mức cược hiện tại.',
|
|
4166
|
+
'Start replay': 'Bắt đầu xem lại',
|
|
4167
|
+
'Total win': 'Tổng thắng',
|
|
4168
|
+
Turbo: 'Turbo',
|
|
4169
|
+
'Ways to win': 'Cách thắng',
|
|
4170
|
+
Win: 'Thắng',
|
|
4171
|
+
'Winning shapes': 'Hình thắng',
|
|
4172
|
+
},
|
|
4173
|
+
zh: {
|
|
4174
|
+
DISCLAIMER: '免责声明',
|
|
4175
|
+
Activate: '激活',
|
|
4176
|
+
Autoplay: '自动游戏',
|
|
4177
|
+
Balance: '余额',
|
|
4178
|
+
Bet: '投注',
|
|
4179
|
+
'BUY BONUS': '购买奖励',
|
|
4180
|
+
Buy: '购买',
|
|
4181
|
+
'Buy bonus': '购买奖励',
|
|
4182
|
+
Cancel: '取消',
|
|
4183
|
+
Close: '关闭',
|
|
4184
|
+
'Cluster pays': '集群赔付',
|
|
4185
|
+
Confirm: '确认',
|
|
4186
|
+
Controls: '控制',
|
|
4187
|
+
'Decrease your stake.': '降低投注额。',
|
|
4188
|
+
DISABLE: '禁用',
|
|
4189
|
+
'Dismiss the current overlay.': '关闭当前弹窗。',
|
|
4190
|
+
'Free spins': '免费旋转',
|
|
4191
|
+
Game: '游戏',
|
|
4192
|
+
'Game info': '游戏信息',
|
|
4193
|
+
Hotkeys: '快捷键',
|
|
4194
|
+
'Increase your stake.': '提高投注额。',
|
|
4195
|
+
'Lower bet': '降低投注',
|
|
4196
|
+
'Master volume': '主音量',
|
|
4197
|
+
'Max win': '最大奖金',
|
|
4198
|
+
Menu: '菜单',
|
|
4199
|
+
'Menu & info': '菜单与信息',
|
|
4200
|
+
Modes: '模式',
|
|
4201
|
+
Music: '音乐',
|
|
4202
|
+
Mute: '静音',
|
|
4203
|
+
'Mute or unmute the game.': '静音或取消静音。',
|
|
4204
|
+
Navigate: '导航',
|
|
4205
|
+
'Open settings and game info.': '打开设置和游戏信息。',
|
|
4206
|
+
'Open the paytable and rules.': '打开赔付表和规则。',
|
|
4207
|
+
'Pay a fixed cost to enter a bonus feature.': '支付固定费用以进入奖励功能。',
|
|
4208
|
+
Paylines: '赔付线',
|
|
4209
|
+
Paytable: '赔付表',
|
|
4210
|
+
'Pays anywhere': '任意位置赢',
|
|
4211
|
+
Price: '价格',
|
|
4212
|
+
'Raise bet': '提高投注',
|
|
4213
|
+
Replay: '重播',
|
|
4214
|
+
RTP: 'RTP',
|
|
4215
|
+
SFX: '音效',
|
|
4216
|
+
Settings: '设置',
|
|
4217
|
+
Sound: '声音',
|
|
4218
|
+
Spin: '旋转',
|
|
4219
|
+
'Spin automatically a set number of times.': '自动旋转设定次数。',
|
|
4220
|
+
'Speed up spin animations.': '加速旋转动画。',
|
|
4221
|
+
Start: '开始',
|
|
4222
|
+
'Start a spin at the current bet.': '以当前投注额开始旋转。',
|
|
4223
|
+
'Start replay': '开始重播',
|
|
4224
|
+
'Total win': '总奖金',
|
|
4225
|
+
Turbo: '急速',
|
|
4226
|
+
'Ways to win': '赢法',
|
|
4227
|
+
Win: '奖金',
|
|
4228
|
+
'Winning shapes': '赢利图形',
|
|
4229
|
+
},
|
|
4230
|
+
};
|
|
4231
|
+
|
|
2717
4232
|
// Social-casino language. English is the source (and, for now, the only) language; `socialize`
|
|
2718
4233
|
// rewrites the restricted gambling vocabulary into social-safe phrasing while preserving case.
|
|
2719
4234
|
//
|
|
@@ -2786,6 +4301,21 @@ function socialize(text) {
|
|
|
2786
4301
|
return repl == null ? m : applyCase(m, repl);
|
|
2787
4302
|
});
|
|
2788
4303
|
}
|
|
4304
|
+
const LANGS = ['de', 'en', 'es', 'fi', 'fr', 'hi', 'id', 'ja', 'ko', 'pl', 'pt', 'ru', 'tr', 'vi', 'zh', 'da'];
|
|
4305
|
+
const LANG_SET = new Set(LANGS);
|
|
4306
|
+
function normalizeLang(code) {
|
|
4307
|
+
const base = (code ?? '').toLowerCase().split(/[-_]/)[0];
|
|
4308
|
+
return (LANG_SET.has(base) ? base : 'en');
|
|
4309
|
+
}
|
|
4310
|
+
function createI18n(opts) {
|
|
4311
|
+
const lang = normalizeLang(opts.language);
|
|
4312
|
+
const t = (src) => {
|
|
4313
|
+
if (lang === 'en')
|
|
4314
|
+
return opts.isSocial ? socialize(src) : src;
|
|
4315
|
+
return opts.messages?.[lang]?.[src] ?? LOCALES[lang]?.[src] ?? src;
|
|
4316
|
+
};
|
|
4317
|
+
return { lang, t };
|
|
4318
|
+
}
|
|
2789
4319
|
|
|
2790
4320
|
const REMOVE_FADE_MS = 300;
|
|
2791
4321
|
class GameShell extends EventEmitter {
|
|
@@ -2801,10 +4331,19 @@ class GameShell extends EventEmitter {
|
|
|
2801
4331
|
prevBalance = 0;
|
|
2802
4332
|
prevWin = 0;
|
|
2803
4333
|
moneyAnims = [];
|
|
2804
|
-
|
|
4334
|
+
kbd;
|
|
4335
|
+
i18n;
|
|
4336
|
+
/** onKey handler of the currently open modal/overlay, if any (set in showModal, cleared in closeModal). */
|
|
4337
|
+
modalOnKey = undefined;
|
|
4338
|
+
/** Shared sound on/off state — Settings speaker toggle and the Shift+M hotkey stay in sync. The
|
|
4339
|
+
* game listens to `settingChange({ key: 'sound' })` to (un)mute audio. */
|
|
4340
|
+
soundOn = true;
|
|
4341
|
+
/** Set by the open Settings modal so Shift+M live-updates its speaker icon; cleared on close. */
|
|
4342
|
+
soundRefresh = null;
|
|
2805
4343
|
constructor(config) {
|
|
2806
4344
|
super();
|
|
2807
4345
|
this.config = config;
|
|
4346
|
+
this.i18n = createI18n({ language: config.language, isSocial: config.isSocial });
|
|
2808
4347
|
this.state = createInitialState(config);
|
|
2809
4348
|
this.styleEl = document.createElement('style');
|
|
2810
4349
|
this.styleEl.textContent = SHELL_CSS;
|
|
@@ -2820,12 +4359,54 @@ class GameShell extends EventEmitter {
|
|
|
2820
4359
|
this.prevWin = this.state.win;
|
|
2821
4360
|
this.observeLayout();
|
|
2822
4361
|
if (typeof document !== 'undefined') {
|
|
2823
|
-
|
|
4362
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
4363
|
+
const shell = this;
|
|
4364
|
+
const host = {
|
|
4365
|
+
get state() { return shell.state; },
|
|
4366
|
+
get hotkeysEnabled() { return shell.config.features.hotkeys !== false; },
|
|
4367
|
+
get spacebarEnabled() { return shell.config.features.spacebar !== false; },
|
|
4368
|
+
get turboLevels() { return shell.config.features.turbo; },
|
|
4369
|
+
get autoplayEnabled() { return shell.config.features.autoplay != null; },
|
|
4370
|
+
get buyBonusEnabled() { return shell.config.features.buyBonus !== false; },
|
|
4371
|
+
hasOpenLayer: () => shell.modalHost.childElementCount > 0,
|
|
4372
|
+
routeToLayer: (e) => shell.modalOnKey?.(e) ?? false,
|
|
4373
|
+
spin: () => shell.emit('spin'),
|
|
4374
|
+
stepBet: (dir) => {
|
|
4375
|
+
const next = stepBet(shell.state, dir);
|
|
4376
|
+
if (next === shell.state.bet)
|
|
4377
|
+
return;
|
|
4378
|
+
shell.state.bet = next;
|
|
4379
|
+
shell.emit('betChange', next);
|
|
4380
|
+
shell.render();
|
|
4381
|
+
},
|
|
4382
|
+
toggleAutoplay: () => {
|
|
4383
|
+
if (shell.state.autoplay.active) {
|
|
4384
|
+
shell.state.autoplay = { active: false, remaining: 0 };
|
|
4385
|
+
shell.emit('autoplayStop');
|
|
4386
|
+
shell.render();
|
|
4387
|
+
}
|
|
4388
|
+
else {
|
|
4389
|
+
shell.openAutoplayPicker();
|
|
4390
|
+
}
|
|
4391
|
+
},
|
|
4392
|
+
cycleTurbo: () => {
|
|
4393
|
+
const next = nextTurbo(shell.state.turbo, shell.config.features.turbo);
|
|
4394
|
+
shell.state.turbo = next;
|
|
4395
|
+
shell.emit('turboChange', next);
|
|
4396
|
+
shell.render();
|
|
4397
|
+
},
|
|
4398
|
+
openBuyBonus: () => shell.openBuyBonus(),
|
|
4399
|
+
openInfo: () => shell.openInfo(),
|
|
4400
|
+
openMenu: () => shell.openMenu(),
|
|
4401
|
+
toggleMute: () => shell.setSound(!shell.soundOn),
|
|
4402
|
+
closeLayer: () => shell.closeModal(),
|
|
4403
|
+
};
|
|
4404
|
+
this.kbd = new KeyboardController(host);
|
|
4405
|
+
this.kbd.attach();
|
|
2824
4406
|
// Stake serves the game in an iframe; on first paint focus is on the HOST page, so a `document`
|
|
2825
4407
|
// keydown never fires and Space scrolls the parent. Pull window focus into the iframe on the
|
|
2826
4408
|
// first pointer interaction so the spacebar shortcut works. Harmless on full-page Energy8.
|
|
2827
4409
|
document.addEventListener('pointerdown', this.pullFocus, true);
|
|
2828
|
-
this.keysBound = true;
|
|
2829
4410
|
}
|
|
2830
4411
|
this.render();
|
|
2831
4412
|
// re-fit once the bundled webfont swaps in (text metrics change → row width changes)
|
|
@@ -2875,10 +4456,11 @@ class GameShell extends EventEmitter {
|
|
|
2875
4456
|
host.classList.remove('ge-fit');
|
|
2876
4457
|
host.style.transform = '';
|
|
2877
4458
|
host.style.transformOrigin = '';
|
|
2878
|
-
// clear any per-zone
|
|
4459
|
+
// clear any per-zone scale/zoom from a prior pass
|
|
2879
4460
|
for (const el of host.querySelectorAll('.ge-zone, .ge-winpill')) {
|
|
2880
4461
|
el.style.transform = '';
|
|
2881
4462
|
el.style.transformOrigin = '';
|
|
4463
|
+
el.style.removeProperty('zoom');
|
|
2882
4464
|
}
|
|
2883
4465
|
if (this.layout === 'mobile') {
|
|
2884
4466
|
// Shrink the whole stack to fit narrow phones (mobile-s, or big balance/win/total-win
|
|
@@ -2896,84 +4478,63 @@ class GameShell extends EventEmitter {
|
|
|
2896
4478
|
}
|
|
2897
4479
|
return;
|
|
2898
4480
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
//
|
|
2902
|
-
//
|
|
4481
|
+
// ONE fit-scale, from the SCREEN SIZE, applied identically in EVERY mode — switching base⇄replay
|
|
4482
|
+
// must not resize the bar. The factor is the frame WIDTH vs the bar's design width, never the
|
|
4483
|
+
// current mode's content width.
|
|
4484
|
+
//
|
|
4485
|
+
// It's applied with `zoom` (not `transform`): zoom shrinks the LAYOUT, so the zones genuinely
|
|
4486
|
+
// take less room and still sit edge-to-edge (menu hard-left, controls hard-right) even when base's
|
|
4487
|
+
// wide row would overflow a merely-visually-scaled bar — so there is no per-mode centred cluster
|
|
4488
|
+
// and no width/mode branching. A wide WIN pill is still lifted above the row first so it can't
|
|
4489
|
+
// shove the controls off-screen. (Mobile, above, keeps its own stacked fit.)
|
|
2903
4490
|
if (pill && bar.scrollWidth > bar.clientWidth + 1) {
|
|
2904
4491
|
host.insertBefore(pill, bar);
|
|
2905
4492
|
pill.classList.add('ge-up');
|
|
2906
4493
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
const naturalH = host.offsetHeight;
|
|
2924
|
-
if (naturalH > availH && naturalH > 0) {
|
|
2925
|
-
const s = (availH / naturalH).toFixed(4);
|
|
2926
|
-
const scaleEdge = (el, origin) => {
|
|
2927
|
-
if (!el)
|
|
2928
|
-
return;
|
|
2929
|
-
el.style.transformOrigin = origin;
|
|
2930
|
-
el.style.transform = `scale(${s})`;
|
|
2931
|
-
};
|
|
2932
|
-
scaleEdge(bar.querySelector('.ge-zone-left'), 'left bottom');
|
|
2933
|
-
scaleEdge(bar.querySelector('.ge-zone-right'), 'right bottom');
|
|
2934
|
-
scaleEdge(host.querySelector('.ge-winpill'), 'center bottom');
|
|
4494
|
+
const zoomBar = (z) => {
|
|
4495
|
+
const v = z < 0.999 ? z.toFixed(4) : '';
|
|
4496
|
+
for (const el of host.querySelectorAll('.ge-zone, .ge-winpill')) {
|
|
4497
|
+
if (v)
|
|
4498
|
+
el.style.setProperty('zoom', v);
|
|
4499
|
+
else
|
|
4500
|
+
el.style.removeProperty('zoom');
|
|
4501
|
+
}
|
|
4502
|
+
};
|
|
4503
|
+
const s = Math.max(GameShell.BAR_MIN_SCALE, Math.min(1, this.root.clientWidth / GameShell.BAR_REF_WIDTH));
|
|
4504
|
+
zoomBar(s);
|
|
4505
|
+
// Safety: a pathologically long balance/win can still overflow the frame at the screen zoom —
|
|
4506
|
+
// nudge the zoom down just enough that the far control (turbo) isn't clipped. Normal content
|
|
4507
|
+
// never triggers this, so base and replay keep the SAME zoom (no size change on mode switch).
|
|
4508
|
+
if (bar.scrollWidth > bar.clientWidth + 1 && bar.scrollWidth > 0) {
|
|
4509
|
+
zoomBar(s * (bar.clientWidth / bar.scrollWidth));
|
|
2935
4510
|
}
|
|
2936
4511
|
}
|
|
2937
|
-
/** Spacebar starts a spin — same path as the spin disc. Ignored when `features.spacebar` is
|
|
2938
|
-
* false, while a spin is running, while autoplay is active, outside base mode, when an
|
|
2939
|
-
* overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
|
|
2940
|
-
* ignored so it can't spam. */
|
|
2941
4512
|
/** Pull window focus into the iframe on first pointer interaction so `document` keydown (the
|
|
2942
4513
|
* spacebar shortcut) fires. No-op / harmless when already focused or full-page. */
|
|
2943
4514
|
pullFocus = () => { try {
|
|
2944
4515
|
window.focus();
|
|
2945
4516
|
}
|
|
2946
4517
|
catch { /* cross-origin / non-browser */ } };
|
|
2947
|
-
handleKeyDown = (e) => {
|
|
2948
|
-
if (this.destroyed || e.code !== 'Space' || e.repeat)
|
|
2949
|
-
return;
|
|
2950
|
-
if (this.config.features.spacebar === false)
|
|
2951
|
-
return; // shortcut disabled (e.g. jurisdiction)
|
|
2952
|
-
const t = e.target;
|
|
2953
|
-
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))
|
|
2954
|
-
return;
|
|
2955
|
-
// Space is ours now — swallow the browser default before any no-op bail. Otherwise the
|
|
2956
|
-
// native "Space activates the focused button" still fires and re-clicks whichever shell
|
|
2957
|
-
// <button> (menu/buy/auto) opened the overlay, tearing down + rebuilding the modal: a
|
|
2958
|
-
// visible flicker. (Also stops the page from scrolling on Space.)
|
|
2959
|
-
e.preventDefault();
|
|
2960
|
-
if (this.modalHost.childElementCount > 0)
|
|
2961
|
-
return; // an overlay/modal is open
|
|
2962
|
-
if (this.state.mode !== 'base' || this.state.busy || this.state.autoplay.active)
|
|
2963
|
-
return;
|
|
2964
|
-
this.emit('spin');
|
|
2965
|
-
};
|
|
2966
4518
|
setLayout(layout) {
|
|
2967
4519
|
if (layout === this.layout)
|
|
2968
4520
|
return;
|
|
2969
4521
|
this.layout = layout;
|
|
2970
4522
|
this.render();
|
|
2971
4523
|
}
|
|
2972
|
-
/** Resolve a built-in shell string
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
4524
|
+
/** Resolve a built-in shell string through the i18n resolver (translation + optional socialize). */
|
|
4525
|
+
t(text) { return this.i18n.t(text); }
|
|
4526
|
+
/** Toggle the social vocabulary at runtime (rebuilds resolver, re-renders bar). */
|
|
4527
|
+
setSocial(isSocial) {
|
|
4528
|
+
this.config.isSocial = isSocial;
|
|
4529
|
+
this.i18n = createI18n({ language: this.config.language, isSocial });
|
|
4530
|
+
this.render();
|
|
4531
|
+
}
|
|
4532
|
+
/** Swap the active language at runtime (rebuilds resolver, re-renders bar). */
|
|
4533
|
+
setLanguage(lang) {
|
|
4534
|
+
this.config.language = lang;
|
|
4535
|
+
this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
|
|
4536
|
+
this.render();
|
|
4537
|
+
}
|
|
2977
4538
|
/** Recolour the shell at runtime (e.g. switch dark/light scheme). */
|
|
2978
4539
|
setTheme(theme) {
|
|
2979
4540
|
this.config.theme = theme;
|
|
@@ -3014,7 +4575,7 @@ class GameShell extends EventEmitter {
|
|
|
3014
4575
|
this.state.mode = mode;
|
|
3015
4576
|
this.render();
|
|
3016
4577
|
}
|
|
3017
|
-
setBusy(busy) { this.state.busy = busy; this.render(); }
|
|
4578
|
+
setBusy(busy) { this.state.busy = busy; this.render(); this.kbd?.notifyBusyChanged(busy); }
|
|
3018
4579
|
setAutoplay(a) { this.state.autoplay = a; this.render(); }
|
|
3019
4580
|
setTurbo(level) { this.state.turbo = level; this.render(); }
|
|
3020
4581
|
/** Currency-aware money formatter for WIN amounts (variable decimals: 0.0041 stays 0.0041, not
|
|
@@ -3022,7 +4583,7 @@ class GameShell extends EventEmitter {
|
|
|
3022
4583
|
formatWin(value) { return formatCurrency(value, this.config.currency, true); }
|
|
3023
4584
|
setBuyBonusEnabled(enabled) { this.state.buyBonusEnabled = enabled; this.render(); }
|
|
3024
4585
|
setFreeSpins(fs) { this.state.freeSpins = fs; this.render(); }
|
|
3025
|
-
showModal(el) {
|
|
4586
|
+
showModal(el, onKey) {
|
|
3026
4587
|
// The control that opened this overlay (menu/buy/auto) keeps DOM focus. Drop it, or a
|
|
3027
4588
|
// stray Space/Enter would natively re-activate that <button> and rebuild the modal — a
|
|
3028
4589
|
// visible flicker. Only relinquish focus we own (a shell control), never the host page's.
|
|
@@ -3031,6 +4592,7 @@ class GameShell extends EventEmitter {
|
|
|
3031
4592
|
active.blur();
|
|
3032
4593
|
this.modalHost.innerHTML = '';
|
|
3033
4594
|
this.modalHost.appendChild(el);
|
|
4595
|
+
this.modalOnKey = onKey;
|
|
3034
4596
|
this.fitModals();
|
|
3035
4597
|
}
|
|
3036
4598
|
/** Uniformly scale every open centred card modal (`.ge-sheet`) down so it fits a short/narrow
|
|
@@ -3045,9 +4607,12 @@ class GameShell extends EventEmitter {
|
|
|
3045
4607
|
/** Fraction of the frame a card modal may occupy; the rest is breathing-room margin. Keeps
|
|
3046
4608
|
* modals from filling a small popout edge-to-edge (so even short pickers scale down there). */
|
|
3047
4609
|
static MODAL_FIT = 0.86;
|
|
3048
|
-
/**
|
|
3049
|
-
*
|
|
3050
|
-
|
|
4610
|
+
/** The bar's design width (px). When the frame is narrower, the bar fit-scales DOWN with the
|
|
4611
|
+
* screen — the SAME factor in every mode, so replay/free-spins shrink like base instead of
|
|
4612
|
+
* staying full-size on a popout. */
|
|
4613
|
+
static BAR_REF_WIDTH = 840;
|
|
4614
|
+
/** Lower bound on the bar fit-scale (guards a degenerate near-zero frame). */
|
|
4615
|
+
static BAR_MIN_SCALE = 0.5;
|
|
3051
4616
|
fitSheet(root) {
|
|
3052
4617
|
const card = root.querySelector('.ge-modal-card');
|
|
3053
4618
|
if (!card)
|
|
@@ -3080,22 +4645,31 @@ class GameShell extends EventEmitter {
|
|
|
3080
4645
|
}
|
|
3081
4646
|
openMenu() { this.emit('menuOpen'); this.openSettings(); }
|
|
3082
4647
|
openSettings() { this.emit('settingsOpen'); this.showModal(openSettingsModal(this)); }
|
|
3083
|
-
openInfo() { this.emit('infoOpen'); this.showModal(
|
|
4648
|
+
openInfo() { this.emit('infoOpen'); const { root, onKey } = openGameInfoModal(this); this.showModal(root, onKey); }
|
|
3084
4649
|
openBuyBonus() {
|
|
3085
4650
|
if (this.config.onBonusBuy) {
|
|
3086
4651
|
this.config.onBonusBuy();
|
|
3087
4652
|
return;
|
|
3088
4653
|
} // game handles it (own UI)
|
|
3089
|
-
const
|
|
3090
|
-
if (
|
|
3091
|
-
this.showModal(
|
|
4654
|
+
const result = openBuyBonusOverlay(this);
|
|
4655
|
+
if (result)
|
|
4656
|
+
this.showModal(result.root, result.onKey);
|
|
3092
4657
|
}
|
|
3093
4658
|
/** Open a generic, externally-driven modal (title + body + optional action buttons).
|
|
3094
4659
|
* Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
|
|
3095
|
-
openModal(opts) { this.showModal(buildModal(opts)); }
|
|
4660
|
+
openModal(opts) { this.showModal(buildModal(opts), opts.onKey); }
|
|
3096
4661
|
/** Programmatically dismiss whatever modal/overlay is currently shown (e.g. auto-close the
|
|
3097
4662
|
* reconnect overlay once the link is restored). No-op when nothing is open. */
|
|
3098
|
-
closeModal() { this.modalHost.innerHTML = ''; }
|
|
4663
|
+
closeModal() { this.modalOnKey = undefined; this.soundRefresh = null; this.modalHost.innerHTML = ''; }
|
|
4664
|
+
/** Flip the shared sound state, notify the game (`settingChange({ key: 'sound' })`), and live-update
|
|
4665
|
+
* the Settings speaker icon if that modal is open. Used by both the Settings toggle and Shift+M. */
|
|
4666
|
+
setSound(on) {
|
|
4667
|
+
this.soundOn = on;
|
|
4668
|
+
this.emit('settingChange', { key: 'sound', value: on });
|
|
4669
|
+
this.soundRefresh?.(on);
|
|
4670
|
+
}
|
|
4671
|
+
/** The Settings modal registers an icon-updater while open (cleared on close). */
|
|
4672
|
+
setSoundRefresh(fn) { this.soundRefresh = fn; }
|
|
3099
4673
|
/** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
|
|
3100
4674
|
openReplay(opts) {
|
|
3101
4675
|
if (this.destroyed)
|
|
@@ -3103,19 +4677,18 @@ class GameShell extends EventEmitter {
|
|
|
3103
4677
|
this.showModal(buildReplayModal(this, opts));
|
|
3104
4678
|
}
|
|
3105
4679
|
/** Bet picker — list of available bets with an accent Confirm. */
|
|
3106
|
-
openBetPicker() { this.showModal(
|
|
4680
|
+
openBetPicker() { const { root, onKey } = openBetModal(this); this.showModal(root, onKey); }
|
|
3107
4681
|
/** Autoplay picker — spin-count list; Confirm starts autoplay. */
|
|
3108
|
-
openAutoplayPicker() { this.showModal(
|
|
4682
|
+
openAutoplayPicker() { const { root, onKey } = openAutoplayModal(this); this.showModal(root, onKey); }
|
|
3109
4683
|
destroy() {
|
|
3110
4684
|
if (this.destroyed)
|
|
3111
4685
|
return Promise.resolve();
|
|
3112
4686
|
this.destroyed = true;
|
|
3113
4687
|
this.ro?.disconnect();
|
|
3114
4688
|
this.ro = null;
|
|
3115
|
-
if (
|
|
3116
|
-
|
|
4689
|
+
if (typeof document !== 'undefined') {
|
|
4690
|
+
this.kbd?.detach();
|
|
3117
4691
|
document.removeEventListener('pointerdown', this.pullFocus, true);
|
|
3118
|
-
this.keysBound = false;
|
|
3119
4692
|
}
|
|
3120
4693
|
this.cancelMoneyAnims();
|
|
3121
4694
|
this.removeAllListeners();
|
|
@@ -3162,7 +4735,7 @@ function removeGameShell() {
|
|
|
3162
4735
|
exports.DevBridge = DevBridge;
|
|
3163
4736
|
exports.EventEmitter = EventEmitter;
|
|
3164
4737
|
exports.GameShell = GameShell;
|
|
3165
|
-
exports.LOADER_BAR_MAX_WIDTH = LOADER_BAR_MAX_WIDTH;
|
|
4738
|
+
exports.LOADER_BAR_MAX_WIDTH = LOADER_BAR_MAX_WIDTH$1;
|
|
3166
4739
|
exports.PlatformSession = PlatformSession;
|
|
3167
4740
|
exports.buildLogoSVG = buildLogoSVG;
|
|
3168
4741
|
exports.createCSSPreloader = createCSSPreloader;
|