@energy8platform/shell 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html.cjs.js +142 -68
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +2 -2
- package/dist/html.esm.js +142 -68
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +131 -68
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +131 -68
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +149 -77
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +2 -2
- package/dist/pixi.esm.js +149 -77
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +4 -1
- package/src/core/i18n.ts +7 -66
- package/src/core/locales.ts +127 -1
- package/src/core/version.ts +1 -1
- package/src/ui/html/shell.css.ts +11 -0
- package/src/ui/pixi/components/BuyBonus.ts +21 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@energy8platform/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Energy8 branded game shell — one logic core, pluggable html/pixi renderers behind a stable contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs.js",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"test:watch": "vitest",
|
|
41
41
|
"prepublishOnly": "npm run build"
|
|
42
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@energy8platform/game-sdk": "^2.9.0"
|
|
45
|
+
},
|
|
43
46
|
"peerDependencies": {
|
|
44
47
|
"pixi.js": "^8.16.0"
|
|
45
48
|
},
|
package/src/core/i18n.ts
CHANGED
|
@@ -1,77 +1,16 @@
|
|
|
1
|
+
import { applySocialReplacements } from '@energy8platform/game-sdk/social';
|
|
1
2
|
import { LOCALES } from './locales';
|
|
2
3
|
|
|
3
4
|
// Social-casino language. English is the source (and, for now, the only) language; `socialize`
|
|
4
5
|
// rewrites the restricted gambling vocabulary into social-safe phrasing while preserving case.
|
|
5
6
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// Conflicting duplicates in the source table are resolved to a single replacement here
|
|
11
|
-
// (betting→playing, total bet→total play, paid out→won, pays out→win).
|
|
12
|
-
const RULES: ReadonlyArray<readonly [string, string]> = [
|
|
13
|
-
['be awarded to player’s accounts', 'appear in player’s accounts'],
|
|
14
|
-
["be awarded to player's accounts", "appear in player's accounts"],
|
|
15
|
-
['place your bets', 'come and play / join in the game'],
|
|
16
|
-
['at the cost of', 'for'],
|
|
17
|
-
['cost of', 'can be played for'],
|
|
18
|
-
['win feature', 'play feature'],
|
|
19
|
-
['total bet', 'total play'],
|
|
20
|
-
['buy bonus', 'get bonus'],
|
|
21
|
-
['bonus buy', 'bonus / feature'],
|
|
22
|
-
['pay out', 'win / won'],
|
|
23
|
-
['paid out', 'won'],
|
|
24
|
-
['pays out', 'win'],
|
|
25
|
-
['payout', 'win'], // single word; "pay out" (spaced) is handled above
|
|
26
|
-
['paytable', 'win table'],
|
|
27
|
-
['paylines', 'winlines'],
|
|
28
|
-
['payline', 'winline'],
|
|
29
|
-
['bet/s', 'play/s'],
|
|
30
|
-
['betting', 'playing'],
|
|
31
|
-
['rebet', 'respin'],
|
|
32
|
-
['stake', 'play amount'],
|
|
33
|
-
['payer', 'winner'],
|
|
34
|
-
['bets', 'plays'],
|
|
35
|
-
['pays', 'wins'],
|
|
36
|
-
['paid', 'won'],
|
|
37
|
-
['bought', 'instantly triggered'],
|
|
38
|
-
['purchase', 'play'],
|
|
39
|
-
['price', 'play'],
|
|
40
|
-
['cost', 'play'], // standalone; the "cost of" / "at the cost of" phrases above win first
|
|
41
|
-
['deposit', 'get coins'],
|
|
42
|
-
['withdraw', 'redeem'],
|
|
43
|
-
['currency', 'token'],
|
|
44
|
-
['gamble', 'play'],
|
|
45
|
-
['wager', 'play'],
|
|
46
|
-
['credit', 'balance'],
|
|
47
|
-
['money', 'coins'],
|
|
48
|
-
['cash', 'coins'],
|
|
49
|
-
['fund', 'balance'],
|
|
50
|
-
['bet', 'play'],
|
|
51
|
-
['pay', 'win'],
|
|
52
|
-
['buy', 'play'],
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const MAP = new Map(RULES.map(([k, v]) => [k.toLowerCase(), v] as const));
|
|
56
|
-
const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
57
|
-
// Letter-bounded so we only swap whole words/phrases (e.g. "pay" inside "Autoplay" is left alone).
|
|
58
|
-
const PATTERN = new RegExp(`(?<![A-Za-z])(?:${RULES.map(([k]) => escapeRe(k)).join('|')})(?![A-Za-z])`, 'gi');
|
|
59
|
-
|
|
60
|
-
/** Carry the matched text's capitalisation onto the replacement: ALL CAPS → upper, Capitalised
|
|
61
|
-
* (first letter) → capitalise the replacement's first letter, otherwise lower-case as written. */
|
|
62
|
-
function applyCase(match: string, repl: string): string {
|
|
63
|
-
const letters = match.replace(/[^A-Za-z]/g, '');
|
|
64
|
-
if (letters && letters === letters.toUpperCase()) return repl.toUpperCase();
|
|
65
|
-
if (/^[^A-Za-z]*[A-Z]/.test(match)) return repl.charAt(0).toUpperCase() + repl.slice(1);
|
|
66
|
-
return repl;
|
|
67
|
-
}
|
|
7
|
+
// The replacement dictionary is NOT kept here — it lives in the platform-wide canonical module
|
|
8
|
+
// `@energy8platform/game-sdk/social`, the same table the game canvas and the Stake bridge use, so
|
|
9
|
+
// the chrome never drifts from the games. Modelled as the `en-social` pseudo-locale in createI18n.
|
|
68
10
|
|
|
69
11
|
/** Rewrite restricted gambling terms in `text` to social-safe phrasing, preserving case. */
|
|
70
12
|
export function socialize(text: string): string {
|
|
71
|
-
return text
|
|
72
|
-
const repl = MAP.get(m.toLowerCase());
|
|
73
|
-
return repl == null ? m : applyCase(m, repl);
|
|
74
|
-
});
|
|
13
|
+
return applySocialReplacements(text);
|
|
75
14
|
}
|
|
76
15
|
|
|
77
16
|
export type Lang = 'de'|'en'|'es'|'fi'|'fr'|'hi'|'id'|'ja'|'ko'|'pl'|'pt'|'ru'|'tr'|'vi'|'zh'|'da';
|
|
@@ -87,6 +26,8 @@ export interface I18nOptions { language: string; isSocial?: boolean; messages?:
|
|
|
87
26
|
export interface I18n { readonly lang: Lang; t(src: string): string; }
|
|
88
27
|
|
|
89
28
|
export function createI18n(opts: I18nOptions): I18n {
|
|
29
|
+
// Social is the `en-social` pseudo-locale: it rewrites the English source. Non-English locales are
|
|
30
|
+
// authored social-safe already, so `socialize` only runs on the English source string.
|
|
90
31
|
const lang = normalizeLang(opts.language);
|
|
91
32
|
const t = (src: string): string => {
|
|
92
33
|
if (lang === 'en') return opts.isSocial ? socialize(src) : src;
|
package/src/core/locales.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Lang } from './i18n';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const BASE_LOCALES: Partial<Record<Lang, Record<string, string>>> = {
|
|
9
9
|
da: {
|
|
10
10
|
DISCLAIMER: 'Ansvarsfraskrivelse',
|
|
11
11
|
Activate: 'Aktivér',
|
|
@@ -862,3 +862,129 @@ export const LOCALES: Partial<Record<Lang, Record<string, string>>> = {
|
|
|
862
862
|
'Winning shapes': '赢利图形',
|
|
863
863
|
},
|
|
864
864
|
};
|
|
865
|
+
|
|
866
|
+
// ── Legal disclaimer body (Stake canonical lines) ──────────────────────────────────────────────
|
|
867
|
+
// Kept in a separate block (grouped per language, not interleaved) because these are long legal
|
|
868
|
+
// sentences, MACHINE-TRANSLATED and pending native QA. The keys are the exact English source lines
|
|
869
|
+
// the Stake bridge emits (see @energy8platform/stake-bridge `buildDisclaimer`). The copyright/brand
|
|
870
|
+
// line ("TM and © {year} Stake Engine.") is deliberately omitted — it renders verbatim.
|
|
871
|
+
const D = {
|
|
872
|
+
L1: 'Malfunction voids all wins and plays.',
|
|
873
|
+
L2: 'A consistent internet connection is required. In the event of a disconnection, reload the game to finish any uncompleted rounds.',
|
|
874
|
+
L3: 'The expected return is calculated over many plays.',
|
|
875
|
+
L4: 'The game display is not representative of any physical device and is for illustrative purposes only.',
|
|
876
|
+
L5: 'Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser.',
|
|
877
|
+
} as const;
|
|
878
|
+
|
|
879
|
+
const DISCLAIMER_LOCALES: Partial<Record<Lang, Record<string, string>>> = {
|
|
880
|
+
da: {
|
|
881
|
+
[D.L1]: 'Fejlfunktion annullerer alle gevinster og spil.',
|
|
882
|
+
[D.L2]: 'En stabil internetforbindelse er påkrævet. I tilfælde af en afbrydelse skal du genindlæse spillet for at afslutte eventuelle uafsluttede runder.',
|
|
883
|
+
[D.L3]: 'Det forventede afkast beregnes over mange spil.',
|
|
884
|
+
[D.L4]: 'Spillets visning svarer ikke til nogen fysisk enhed og er kun til illustrative formål.',
|
|
885
|
+
[D.L5]: 'Gevinster afregnes i henhold til det beløb, der modtages fra Remote Game Server, og ikke ud fra hændelser i webbrowseren.',
|
|
886
|
+
},
|
|
887
|
+
de: {
|
|
888
|
+
[D.L1]: 'Eine Fehlfunktion macht alle Gewinne und Spiele ungültig.',
|
|
889
|
+
[D.L2]: 'Eine stabile Internetverbindung ist erforderlich. Im Falle einer Unterbrechung lade das Spiel neu, um nicht abgeschlossene Runden zu beenden.',
|
|
890
|
+
[D.L3]: 'Der erwartete Ertrag wird über viele Spiele berechnet.',
|
|
891
|
+
[D.L4]: 'Die Spieldarstellung entspricht keinem physischen Gerät und dient nur zur Veranschaulichung.',
|
|
892
|
+
[D.L5]: 'Gewinne werden gemäß dem vom Remote Game Server erhaltenen Betrag abgerechnet und nicht anhand von Ereignissen im Webbrowser.',
|
|
893
|
+
},
|
|
894
|
+
es: {
|
|
895
|
+
[D.L1]: 'Un fallo anula todas las ganancias y jugadas.',
|
|
896
|
+
[D.L2]: 'Se requiere una conexión a internet estable. En caso de desconexión, vuelve a cargar el juego para terminar las rondas no completadas.',
|
|
897
|
+
[D.L3]: 'El retorno esperado se calcula a lo largo de muchas jugadas.',
|
|
898
|
+
[D.L4]: 'La visualización del juego no representa ningún dispositivo físico y tiene fines meramente ilustrativos.',
|
|
899
|
+
[D.L5]: 'Las ganancias se liquidan según el importe recibido del Remote Game Server y no según los eventos ocurridos en el navegador web.',
|
|
900
|
+
},
|
|
901
|
+
fi: {
|
|
902
|
+
[D.L1]: 'Toimintahäiriö mitätöi kaikki voitot ja pelit.',
|
|
903
|
+
[D.L2]: 'Vakaa internetyhteys vaaditaan. Jos yhteys katkeaa, lataa peli uudelleen suorittaaksesi keskeneräiset kierrokset loppuun.',
|
|
904
|
+
[D.L3]: 'Odotettu palautus lasketaan monen pelin perusteella.',
|
|
905
|
+
[D.L4]: 'Pelin näkymä ei vastaa mitään fyysistä laitetta ja on tarkoitettu vain havainnollistamiseen.',
|
|
906
|
+
[D.L5]: 'Voitot maksetaan Remote Game Serverin ilmoittaman summan mukaan, ei verkkoselaimen tapahtumien perusteella.',
|
|
907
|
+
},
|
|
908
|
+
fr: {
|
|
909
|
+
[D.L1]: 'Un dysfonctionnement annule tous les gains et les parties.',
|
|
910
|
+
[D.L2]: 'Une connexion internet stable est requise. En cas de déconnexion, rechargez le jeu pour terminer les tours non achevés.',
|
|
911
|
+
[D.L3]: 'Le retour attendu est calculé sur de nombreuses parties.',
|
|
912
|
+
[D.L4]: "L'affichage du jeu ne représente aucun appareil physique et sert uniquement à des fins d'illustration.",
|
|
913
|
+
[D.L5]: 'Les gains sont réglés en fonction du montant reçu du Remote Game Server et non des événements survenus dans le navigateur web.',
|
|
914
|
+
},
|
|
915
|
+
hi: {
|
|
916
|
+
[D.L1]: 'खराबी सभी जीत और खेलों को रद्द कर देती है।',
|
|
917
|
+
[D.L2]: 'एक स्थिर इंटरनेट कनेक्शन आवश्यक है। कनेक्शन टूटने की स्थिति में, अधूरे राउंड पूरे करने के लिए गेम को फिर से लोड करें।',
|
|
918
|
+
[D.L3]: 'अपेक्षित रिटर्न की गणना कई खेलों पर की जाती है।',
|
|
919
|
+
[D.L4]: 'गेम का प्रदर्शन किसी भौतिक उपकरण का प्रतिनिधित्व नहीं करता और केवल उदाहरण के उद्देश्य से है।',
|
|
920
|
+
[D.L5]: 'जीत का निपटान Remote Game Server से प्राप्त राशि के अनुसार किया जाता है, न कि वेब ब्राउज़र के भीतर की घटनाओं के आधार पर।',
|
|
921
|
+
},
|
|
922
|
+
id: {
|
|
923
|
+
[D.L1]: 'Malfungsi membatalkan semua kemenangan dan permainan.',
|
|
924
|
+
[D.L2]: 'Koneksi internet yang stabil diperlukan. Jika terjadi pemutusan koneksi, muat ulang permainan untuk menyelesaikan ronde yang belum selesai.',
|
|
925
|
+
[D.L3]: 'Perkiraan pengembalian dihitung dari banyak permainan.',
|
|
926
|
+
[D.L4]: 'Tampilan permainan tidak mewakili perangkat fisik mana pun dan hanya untuk tujuan ilustrasi.',
|
|
927
|
+
[D.L5]: 'Kemenangan diselesaikan sesuai dengan jumlah yang diterima dari Remote Game Server dan bukan dari peristiwa di dalam peramban web.',
|
|
928
|
+
},
|
|
929
|
+
ja: {
|
|
930
|
+
[D.L1]: '誤作動が発生した場合、すべての勝利とプレイは無効となります。',
|
|
931
|
+
[D.L2]: '安定したインターネット接続が必要です。接続が切断された場合は、ゲームを再読み込みして未完了のラウンドを終了してください。',
|
|
932
|
+
[D.L3]: '期待される還元率は多数のプレイにわたって計算されます。',
|
|
933
|
+
[D.L4]: 'ゲームの表示は物理的な機器を表すものではなく、あくまで説明のためのものです。',
|
|
934
|
+
[D.L5]: '賞金はウェブブラウザ内のイベントではなく、Remote Game Server から受信した金額に基づいて確定されます。',
|
|
935
|
+
},
|
|
936
|
+
ko: {
|
|
937
|
+
[D.L1]: '오작동이 발생하면 모든 당첨과 플레이가 무효가 됩니다.',
|
|
938
|
+
[D.L2]: '안정적인 인터넷 연결이 필요합니다. 연결이 끊긴 경우 게임을 새로 고침하여 완료되지 않은 라운드를 마치십시오.',
|
|
939
|
+
[D.L3]: '예상 환급률은 여러 번의 플레이에 걸쳐 계산됩니다.',
|
|
940
|
+
[D.L4]: '게임 화면은 실제 기기를 나타내지 않으며 설명을 위한 용도로만 제공됩니다.',
|
|
941
|
+
[D.L5]: '당첨금은 웹 브라우저 내의 이벤트가 아니라 Remote Game Server에서 수신한 금액에 따라 정산됩니다.',
|
|
942
|
+
},
|
|
943
|
+
pl: {
|
|
944
|
+
[D.L1]: 'Awaria unieważnia wszystkie wygrane i gry.',
|
|
945
|
+
[D.L2]: 'Wymagane jest stabilne połączenie internetowe. W przypadku rozłączenia załaduj grę ponownie, aby dokończyć niezakończone rundy.',
|
|
946
|
+
[D.L3]: 'Oczekiwany zwrot jest obliczany na przestrzeni wielu gier.',
|
|
947
|
+
[D.L4]: 'Wyświetlanie gry nie odzwierciedla żadnego fizycznego urządzenia i służy wyłącznie celom ilustracyjnym.',
|
|
948
|
+
[D.L5]: 'Wygrane są rozliczane zgodnie z kwotą otrzymaną z Remote Game Server, a nie na podstawie zdarzeń w przeglądarce internetowej.',
|
|
949
|
+
},
|
|
950
|
+
pt: {
|
|
951
|
+
[D.L1]: 'Uma falha anula todos os ganhos e jogadas.',
|
|
952
|
+
[D.L2]: 'É necessária uma ligação à internet estável. Em caso de desconexão, recarregue o jogo para concluir as rondas não terminadas.',
|
|
953
|
+
[D.L3]: 'O retorno esperado é calculado ao longo de muitas jogadas.',
|
|
954
|
+
[D.L4]: 'A apresentação do jogo não representa nenhum dispositivo físico e destina-se apenas a fins ilustrativos.',
|
|
955
|
+
[D.L5]: 'Os ganhos são liquidados de acordo com o valor recebido do Remote Game Server e não com base em eventos ocorridos no navegador web.',
|
|
956
|
+
},
|
|
957
|
+
ru: {
|
|
958
|
+
[D.L1]: 'Сбой аннулирует все выигрыши и игры.',
|
|
959
|
+
[D.L2]: 'Требуется стабильное интернет-соединение. В случае разрыва соединения перезагрузите игру, чтобы завершить незаконченные раунды.',
|
|
960
|
+
[D.L3]: 'Ожидаемый возврат рассчитывается на основе множества игр.',
|
|
961
|
+
[D.L4]: 'Отображение игры не соответствует какому-либо физическому устройству и приводится исключительно в иллюстративных целях.',
|
|
962
|
+
[D.L5]: 'Выигрыши начисляются в соответствии с суммой, полученной от Remote Game Server, а не на основе событий в веб-браузере.',
|
|
963
|
+
},
|
|
964
|
+
tr: {
|
|
965
|
+
[D.L1]: 'Arıza, tüm kazançları ve oyunları geçersiz kılar.',
|
|
966
|
+
[D.L2]: 'Kararlı bir internet bağlantısı gereklidir. Bağlantının kesilmesi durumunda, tamamlanmamış turları bitirmek için oyunu yeniden yükleyin.',
|
|
967
|
+
[D.L3]: 'Beklenen getiri, çok sayıda oyun üzerinden hesaplanır.',
|
|
968
|
+
[D.L4]: 'Oyun görüntüsü herhangi bir fiziksel cihazı temsil etmez ve yalnızca açıklama amaçlıdır.',
|
|
969
|
+
[D.L5]: "Kazançlar, web tarayıcısındaki olaylara göre değil, Remote Game Server'dan alınan tutara göre ödenir.",
|
|
970
|
+
},
|
|
971
|
+
vi: {
|
|
972
|
+
[D.L1]: 'Sự cố sẽ hủy bỏ mọi phần thắng và lượt chơi.',
|
|
973
|
+
[D.L2]: 'Cần có kết nối internet ổn định. Trong trường hợp mất kết nối, hãy tải lại trò chơi để hoàn tất các vòng chưa hoàn thành.',
|
|
974
|
+
[D.L3]: 'Lợi nhuận kỳ vọng được tính trên nhiều lượt chơi.',
|
|
975
|
+
[D.L4]: 'Hiển thị của trò chơi không đại diện cho bất kỳ thiết bị vật lý nào và chỉ nhằm mục đích minh họa.',
|
|
976
|
+
[D.L5]: 'Phần thắng được thanh toán theo số tiền nhận được từ Remote Game Server chứ không dựa trên các sự kiện trong trình duyệt web.',
|
|
977
|
+
},
|
|
978
|
+
zh: {
|
|
979
|
+
[D.L1]: '故障将使所有奖金和游戏无效。',
|
|
980
|
+
[D.L2]: '需要稳定的网络连接。如果连接中断,请重新加载游戏以完成未结束的回合。',
|
|
981
|
+
[D.L3]: '预期回报是根据多次游戏计算得出的。',
|
|
982
|
+
[D.L4]: '游戏显示并不代表任何实体设备,仅供说明之用。',
|
|
983
|
+
[D.L5]: '奖金根据从 Remote Game Server 收到的金额结算,而非依据网页浏览器内的事件。',
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
/** Merge the base UI catalog with the separately-authored disclaimer lines, per language. */
|
|
988
|
+
export const LOCALES: Partial<Record<Lang, Record<string, string>>> = Object.fromEntries(
|
|
989
|
+
(Object.keys(BASE_LOCALES) as Lang[]).map((l) => [l, { ...BASE_LOCALES[l], ...DISCLAIMER_LOCALES[l] }]),
|
|
990
|
+
);
|
package/src/core/version.ts
CHANGED
package/src/ui/html/shell.css.ts
CHANGED
|
@@ -397,6 +397,17 @@ export const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
397
397
|
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval { min-width:clamp(50px,14cqh,66px); }
|
|
398
398
|
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval b { font-size:clamp(9px,2.5cqh,11px); }
|
|
399
399
|
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-bb-betval span { font-size:clamp(5px,1.25cqh,6px); }
|
|
400
|
+
/* Popout S (very short landscape, e.g. 400×225): the height-fit shrank the cards' descriptions to an
|
|
401
|
+
unreadable ~4px floor with nothing to scroll. Below a ~340px frame height, drop the shrink-to-fit and
|
|
402
|
+
switch the (non-mobile) buy-bonus to a READABLE vertical stack that scrolls vertically — like mobile.
|
|
403
|
+
Query the overlay's ge-bb-frame size container so it tracks the popout frame, not the browser window. */
|
|
404
|
+
@container ge-bb-frame (max-height: 340px) {
|
|
405
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) [data-ge="buybonus-overlay"] .ge-ov-scroll { overflow-y:auto; }
|
|
406
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) [data-ge="buybonus-overlay"] .ge-ov-body { justify-content:flex-start; }
|
|
407
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) .ge-bb-grid { flex-direction:column; align-items:center; overflow:visible; }
|
|
408
|
+
#${SHELL_ROOT_ID}:not(.ge-mobile) .ge-bb-grid .ge-bonus-card {
|
|
409
|
+
flex:0 0 auto; width:18em; font-size:min(12px, calc(84cqw / 18)); }
|
|
410
|
+
}
|
|
400
411
|
|
|
401
412
|
/* ═══ desktop control bar — one continuous dark panel; white-disc buttons; BUY BONUS outside-left ═══ */
|
|
402
413
|
/* the dark surface fills the row width (BUY BONUS sits outside, to its left) */
|
|
@@ -10,6 +10,11 @@ import { clamp } from '../primitives/overlay';
|
|
|
10
10
|
import { attachHover } from '../primitives/widgets';
|
|
11
11
|
import { roundedPath } from '../primitives/flex';
|
|
12
12
|
|
|
13
|
+
/** Below this frame height (px), a wide/landscape popout (e.g. Popout S 400×225) stacks its cards
|
|
14
|
+
* vertically and scrolls — like mobile — so descriptions stay readable instead of shrinking to a
|
|
15
|
+
* ~4px floor. Wide + taller frames (Popout L and up) keep the centred horizontal row. */
|
|
16
|
+
const SHORT_STACK_H = 340;
|
|
17
|
+
|
|
13
18
|
/** Buy-bonus overlay — art-forward cards (one per option), a live bet footer, and a confirm modal.
|
|
14
19
|
* Returns null when there are no bonus options. */
|
|
15
20
|
export function openBuyBonus(host: PixiComponentContext): ShellLayer | null {
|
|
@@ -46,6 +51,8 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
46
51
|
private cardEntries: CardEntry[] = [];
|
|
47
52
|
/** Keyboard focus index into the affordable subset of cardEntries. -1 = none. */
|
|
48
53
|
private focusIndex = -1;
|
|
54
|
+
/** true when cards are stacked vertically (mobile, or a short landscape popout). */
|
|
55
|
+
private stack = false;
|
|
49
56
|
// Drag-scroll state. The drag is handled on the (unmasked) overlay, not the masked strip — a mask
|
|
50
57
|
// prunes pointer events outside its band, stalling globalpointermove mid-drag so later cards never
|
|
51
58
|
// scroll into reach. The overlay sees the whole screen, so the scroll runs the full range.
|
|
@@ -155,7 +162,10 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
155
162
|
|
|
156
163
|
private buildCards(): void {
|
|
157
164
|
this.strip.removeChildren().forEach((c) => c.destroy({ children: true }));
|
|
158
|
-
|
|
165
|
+
// Stack (readable vertical list + scroll) on mobile OR on a short landscape popout; a shrink-to-fit
|
|
166
|
+
// horizontal row on wide + tall frames. See SHORT_STACK_H.
|
|
167
|
+
const stack = this.host.layout === 'mobile' || this.h <= SHORT_STACK_H;
|
|
168
|
+
this.stack = stack;
|
|
159
169
|
const top = this.headerH + 6;
|
|
160
170
|
const areaH = this.h - top - this.footerH - 6;
|
|
161
171
|
const gap = 14;
|
|
@@ -166,13 +176,13 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
166
176
|
// N cards (+ gaps + 24px side margins) fit the frame width. min() of the two = the binding fit.
|
|
167
177
|
// Floor 4 is a last-resort so a 400×225 popout still shows the CTA (then X-drag scrolls the slack).
|
|
168
178
|
const emH = 3.4 * (areaH / 100);
|
|
169
|
-
const emW =
|
|
179
|
+
const emW = stack
|
|
170
180
|
? (this.w - 48) / 18 // vertical stack: a single card spans the width
|
|
171
181
|
: (this.w - 48 - (n - 1) * gap) / (18 * n); // row: N cards + gaps fit the frame width
|
|
172
|
-
const em =
|
|
182
|
+
const em = stack ? Math.min(12, emW) : clamp(4, Math.min(emH, emW), 12);
|
|
173
183
|
const cardW = Math.min(18 * em, this.w - 48);
|
|
174
184
|
|
|
175
|
-
const cards = this.bonuses.map((b) => this.buildCard(b, cardW, em,
|
|
185
|
+
const cards = this.bonuses.map((b) => this.buildCard(b, cardW, em, stack, areaH));
|
|
176
186
|
const cardH = Math.max(...cards.map((c) => c.height));
|
|
177
187
|
for (const c of cards) c.setHeight(cardH);
|
|
178
188
|
|
|
@@ -193,7 +203,7 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
this.stripMask.clear();
|
|
196
|
-
if (
|
|
206
|
+
if (stack) {
|
|
197
207
|
// vertical stack — scroll vertically if it overflows (simple drag)
|
|
198
208
|
let y = 0;
|
|
199
209
|
const colX = (this.w - cardW) / 2;
|
|
@@ -220,7 +230,7 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
// ── one card ──────────────────────────────────────────────────────────────
|
|
223
|
-
private buildCard(bonus: BonusOption, cardW: number, em: number,
|
|
233
|
+
private buildCard(bonus: BonusOption, cardW: number, em: number, stack: boolean, areaH: number): CardView {
|
|
224
234
|
const accent = effectiveAccent(bonus);
|
|
225
235
|
const ink = contrastText(accent);
|
|
226
236
|
const price = bonus.priceMultiplier * this.host.state.bet;
|
|
@@ -255,7 +265,7 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
255
265
|
ctaLabel: this.host.t(bonus.type === 'feature' ? 'Activate' : 'Buy'),
|
|
256
266
|
onSelect: select,
|
|
257
267
|
});
|
|
258
|
-
void
|
|
268
|
+
void stack;
|
|
259
269
|
void areaH;
|
|
260
270
|
return card;
|
|
261
271
|
}
|
|
@@ -435,7 +445,8 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
435
445
|
* Browse phase: arrows move focus; +/- step bet; Enter/Space opens confirm; Escape closes.
|
|
436
446
|
* Confirm phase: Enter/Space buys/activates; Escape returns to browse. */
|
|
437
447
|
onKey(e: KeyboardEvent): boolean {
|
|
438
|
-
|
|
448
|
+
// Vertical stack (mobile / short popout) → Up/Down navigate; horizontal row → Left/Right.
|
|
449
|
+
const stack = this.stack;
|
|
439
450
|
|
|
440
451
|
if (this.confirm && this.confirmBonus) {
|
|
441
452
|
// ── Confirm phase ──
|
|
@@ -467,8 +478,8 @@ class BuyBonusOverlay extends Container implements ShellLayer {
|
|
|
467
478
|
if (bet !== null) { this.stepBetBy(bet); return true; }
|
|
468
479
|
|
|
469
480
|
// Determine navigation direction from key code + layout
|
|
470
|
-
const fwdKey = e.code === 'ArrowRight' || (
|
|
471
|
-
const bwdKey = e.code === 'ArrowLeft' || (
|
|
481
|
+
const fwdKey = e.code === 'ArrowRight' || (stack && e.code === 'ArrowDown');
|
|
482
|
+
const bwdKey = e.code === 'ArrowLeft' || (stack && e.code === 'ArrowUp');
|
|
472
483
|
|
|
473
484
|
if (fwdKey) {
|
|
474
485
|
if (last < 0) return true;
|