@stakeplate/core 0.1.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/MockNetworkManager-cj8WT5v0.js +126 -0
- package/dist/MockNetworkManager-cj8WT5v0.js.map +1 -0
- package/dist/audio.d.ts +2 -0
- package/dist/audio.js +112 -0
- package/dist/audio.js.map +1 -0
- package/dist/bind-BBRxizWR.js +43 -0
- package/dist/bind-BBRxizWR.js.map +1 -0
- package/dist/hud-port-DNfotn4U.d.ts +49 -0
- package/dist/index-BBlN97RY.d.ts +58 -0
- package/dist/index-BcTRfis9.d.ts +276 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +561 -0
- package/dist/index.js.map +1 -0
- package/dist/network-pBW8CX1o.d.ts +120 -0
- package/dist/protocol-C1KamF3t.d.ts +112 -0
- package/dist/rgs.d.ts +25 -0
- package/dist/rgs.js +119 -0
- package/dist/rgs.js.map +1 -0
- package/dist/rules.d.ts +68 -0
- package/dist/rules.js +250 -0
- package/dist/rules.js.map +1 -0
- package/dist/stores.d.ts +2 -0
- package/dist/stores.js +93 -0
- package/dist/stores.js.map +1 -0
- package/dist/testing.d.ts +18 -0
- package/dist/testing.js +38 -0
- package/dist/testing.js.map +1 -0
- package/dist/ticker-A-XgayZv.js +26 -0
- package/dist/ticker-A-XgayZv.js.map +1 -0
- package/package.json +118 -0
package/dist/rules.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
//#region src/rules/social.ts
|
|
2
|
+
/** Restricted → replacement, ORDERED longest-phrase-first (so multi-word phrases win). */
|
|
3
|
+
const SOCIAL_REPLACEMENTS = [
|
|
4
|
+
["be awarded to player's accounts", "appear in player's accounts"],
|
|
5
|
+
["place your bets", "come and play / join in the game"],
|
|
6
|
+
["at the cost of", "for"],
|
|
7
|
+
["bonus buy", "bonus / feature"],
|
|
8
|
+
["buy bonus", "get bonus"],
|
|
9
|
+
["win feature", "play feature"],
|
|
10
|
+
["total bet", "total play"],
|
|
11
|
+
["pays out", "won"],
|
|
12
|
+
["paid out", "win"],
|
|
13
|
+
["pay out", "win"],
|
|
14
|
+
["cost of", "can be played for"],
|
|
15
|
+
["paytable", "prize table"],
|
|
16
|
+
["payouts", "wins"],
|
|
17
|
+
["payout", "win"],
|
|
18
|
+
["betting", "playing"],
|
|
19
|
+
["gambling", "playing"],
|
|
20
|
+
["wagering", "playing"],
|
|
21
|
+
["paying", "awarding"],
|
|
22
|
+
["buying", "playing"],
|
|
23
|
+
["buys", "plays"],
|
|
24
|
+
["deposit", "get coins"],
|
|
25
|
+
["withdraw", "redeem"],
|
|
26
|
+
["purchase", "play"],
|
|
27
|
+
["currency", "token"],
|
|
28
|
+
["credits", "coins"],
|
|
29
|
+
["credit", "coins"],
|
|
30
|
+
["winnings", "prizes"],
|
|
31
|
+
["payer", "winner"],
|
|
32
|
+
["bought", "instantly triggered"],
|
|
33
|
+
["gamble", "play"],
|
|
34
|
+
["wager", "play"],
|
|
35
|
+
["rebet", "respin"],
|
|
36
|
+
["stakes", "play amounts"],
|
|
37
|
+
["stake", "play amount"],
|
|
38
|
+
["money", "coins"],
|
|
39
|
+
["cash", "coins"],
|
|
40
|
+
["bets", "plays"],
|
|
41
|
+
["bet", "play"],
|
|
42
|
+
["pays", "wins"],
|
|
43
|
+
["paid", "won"],
|
|
44
|
+
["pay", "win"],
|
|
45
|
+
["buy", "play"]
|
|
46
|
+
];
|
|
47
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
48
|
+
const matchCase = (matched, repl) => matched[0] && matched[0] === matched[0].toUpperCase() && matched[0] !== matched[0].toLowerCase() ? repl.charAt(0).toUpperCase() + repl.slice(1) : repl;
|
|
49
|
+
const REPLACERS = SOCIAL_REPLACEMENTS.map(([term, repl]) => [new RegExp(`\\b${escapeRe(term)}\\b`, "gi"), repl]);
|
|
50
|
+
const RESTRICTED = SOCIAL_REPLACEMENTS.map(([term]) => term);
|
|
51
|
+
/**
|
|
52
|
+
* Rewrite an English string into its social/sweepstakes wording — restricted phrases swapped
|
|
53
|
+
* for their alternatives (word-boundary safe, longest-phrase-first, case-preserving). Idempotent
|
|
54
|
+
* enough for authoring: run it over your menu/feature copy to auto-generate social variants.
|
|
55
|
+
*/
|
|
56
|
+
function toSocial(text) {
|
|
57
|
+
let out = text;
|
|
58
|
+
for (const [re, repl] of REPLACERS) out = out.replace(re, (m) => matchCase(m, repl));
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* AGGRESSIVELY find restricted phrases in an English string — matches even as part of a word
|
|
63
|
+
* (so `paytable`, `betting`, `payout` are all flagged). Returns the offending terms. Use it at
|
|
64
|
+
* launch/build to fail on any social-copy that would still surface gambling wording.
|
|
65
|
+
*/
|
|
66
|
+
function findRestricted(text) {
|
|
67
|
+
const lower = text.toLowerCase();
|
|
68
|
+
const hits = [];
|
|
69
|
+
for (const term of RESTRICTED) if (lower.includes(term)) hits.push(term);
|
|
70
|
+
return hits;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/rules/index.ts
|
|
74
|
+
/** Every interactive control described (Stake: the Info/Help must explain each button). */
|
|
75
|
+
const DEFAULT_CONTROL_GUIDE = [
|
|
76
|
+
"**Spin** — plays one round at your current bet; press and hold to spin in turbo.",
|
|
77
|
+
"**+ / −** — raise or lower your bet before spinning.",
|
|
78
|
+
"**Autoplay** — plays a set number of rounds automatically; tap again to stop any time.",
|
|
79
|
+
"**Turbo / Quick spin** — speeds up spins by shortening the animation; the result is identical.",
|
|
80
|
+
"**Buy Feature** — instantly buys entry to the bonus for the shown price (needs confirmation).",
|
|
81
|
+
"**Menu** — opens settings, the paytable and these rules.",
|
|
82
|
+
"**Sound** — mutes or unmutes all game audio."
|
|
83
|
+
];
|
|
84
|
+
/** The EXACT Stake Engine disclaimer — never reworded, never given a social override. */
|
|
85
|
+
const STAKE_DISCLAIMER = "Malfunction voids all wins and plays. A consistent internet connection is required. In the event of a disconnection, reload the game to finish any uncompleted rounds. The expected return is calculated over many plays. The game display is not representative of any physical device and is for illustrative purposes only. Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser. TM and © 2026 Stake Engine.";
|
|
86
|
+
/** Collect every translatable English string in a block tree (for social auto-derivation). */
|
|
87
|
+
function collectStrings(blocks, out = []) {
|
|
88
|
+
for (const b of blocks) switch (b.kind) {
|
|
89
|
+
case "text":
|
|
90
|
+
case "heading":
|
|
91
|
+
case "subheading":
|
|
92
|
+
case "legal":
|
|
93
|
+
out.push(b.text);
|
|
94
|
+
break;
|
|
95
|
+
case "callout":
|
|
96
|
+
if (b.title) out.push(b.title);
|
|
97
|
+
out.push(b.text);
|
|
98
|
+
break;
|
|
99
|
+
case "steps":
|
|
100
|
+
out.push(...b.items);
|
|
101
|
+
break;
|
|
102
|
+
case "stat-grid":
|
|
103
|
+
for (const it of b.items) out.push(it.label, it.value);
|
|
104
|
+
break;
|
|
105
|
+
case "cards":
|
|
106
|
+
for (const it of b.items) {
|
|
107
|
+
out.push(it.title);
|
|
108
|
+
if (it.text) out.push(it.text);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case "table":
|
|
112
|
+
if (b.columns) out.push(...b.columns);
|
|
113
|
+
for (const r of b.rows) out.push(...r);
|
|
114
|
+
break;
|
|
115
|
+
case "media":
|
|
116
|
+
if (b.title) out.push(b.title);
|
|
117
|
+
out.push(b.text);
|
|
118
|
+
break;
|
|
119
|
+
case "paytable":
|
|
120
|
+
for (const r of b.rows) if (r.symbol) out.push(r.symbol);
|
|
121
|
+
break;
|
|
122
|
+
case "group":
|
|
123
|
+
if (b.title) out.push(b.title);
|
|
124
|
+
collectStrings(b.children, out);
|
|
125
|
+
break;
|
|
126
|
+
default: break;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function buildRules(opts = {}) {
|
|
131
|
+
const rules = [];
|
|
132
|
+
if (opts.about) {
|
|
133
|
+
rules.push({
|
|
134
|
+
kind: "heading",
|
|
135
|
+
id: "r-about-h",
|
|
136
|
+
text: "About the game"
|
|
137
|
+
});
|
|
138
|
+
rules.push({
|
|
139
|
+
kind: "text",
|
|
140
|
+
id: "r-about",
|
|
141
|
+
text: opts.about
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (opts.howToPlay?.length) {
|
|
145
|
+
rules.push({
|
|
146
|
+
kind: "heading",
|
|
147
|
+
id: "r-play-h",
|
|
148
|
+
text: "How to play"
|
|
149
|
+
});
|
|
150
|
+
rules.push({
|
|
151
|
+
kind: "steps",
|
|
152
|
+
id: "r-play",
|
|
153
|
+
ordered: true,
|
|
154
|
+
items: opts.howToPlay
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (opts.features?.length) {
|
|
158
|
+
rules.push({
|
|
159
|
+
kind: "heading",
|
|
160
|
+
id: "r-feat-h",
|
|
161
|
+
text: "Features"
|
|
162
|
+
});
|
|
163
|
+
rules.push({
|
|
164
|
+
kind: "cards",
|
|
165
|
+
id: "r-feat",
|
|
166
|
+
items: opts.features
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
rules.push({
|
|
170
|
+
kind: "heading",
|
|
171
|
+
id: "r-ctrl-h",
|
|
172
|
+
text: "Controls"
|
|
173
|
+
});
|
|
174
|
+
rules.push({
|
|
175
|
+
kind: "text",
|
|
176
|
+
id: "r-ctrl-0",
|
|
177
|
+
text: "Every interactive button and what it does:"
|
|
178
|
+
});
|
|
179
|
+
rules.push({
|
|
180
|
+
kind: "steps",
|
|
181
|
+
id: "r-ctrl",
|
|
182
|
+
ordered: false,
|
|
183
|
+
items: opts.controlGuide ?? DEFAULT_CONTROL_GUIDE
|
|
184
|
+
});
|
|
185
|
+
const stats = opts.stats;
|
|
186
|
+
if (stats) {
|
|
187
|
+
const items = [];
|
|
188
|
+
if (stats.rtp) items.push({
|
|
189
|
+
label: "RTP",
|
|
190
|
+
value: stats.rtp
|
|
191
|
+
});
|
|
192
|
+
if (stats.volatility) items.push({
|
|
193
|
+
label: "Volatility",
|
|
194
|
+
value: stats.volatility
|
|
195
|
+
});
|
|
196
|
+
if (stats.maxWin) items.push({
|
|
197
|
+
label: "Max win",
|
|
198
|
+
value: stats.maxWin
|
|
199
|
+
});
|
|
200
|
+
if (stats.lines) items.push({
|
|
201
|
+
label: "Lines",
|
|
202
|
+
value: stats.lines
|
|
203
|
+
});
|
|
204
|
+
if (stats.extra) items.push(...stats.extra);
|
|
205
|
+
if (items.length) {
|
|
206
|
+
rules.push({
|
|
207
|
+
kind: "heading",
|
|
208
|
+
id: "r-info-h",
|
|
209
|
+
text: "Game info"
|
|
210
|
+
});
|
|
211
|
+
rules.push({
|
|
212
|
+
kind: "stat-grid",
|
|
213
|
+
id: "r-info",
|
|
214
|
+
items
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (opts.extra?.length) rules.push(...opts.extra);
|
|
219
|
+
if (opts.disclaimer !== false) {
|
|
220
|
+
rules.push({
|
|
221
|
+
kind: "heading",
|
|
222
|
+
id: "r-general-h",
|
|
223
|
+
text: "General"
|
|
224
|
+
});
|
|
225
|
+
rules.push({
|
|
226
|
+
kind: "text",
|
|
227
|
+
id: "r-general",
|
|
228
|
+
text: STAKE_DISCLAIMER
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const menu = {
|
|
232
|
+
...opts.sound ? { sound: opts.sound } : {},
|
|
233
|
+
...opts.paytable ? { paytable: opts.paytable } : {},
|
|
234
|
+
rules
|
|
235
|
+
};
|
|
236
|
+
const socialEn = {};
|
|
237
|
+
for (const s of collectStrings([...rules, ...opts.paytable ?? []])) {
|
|
238
|
+
if (s === "Malfunction voids all wins and plays. A consistent internet connection is required. In the event of a disconnection, reload the game to finish any uncompleted rounds. The expected return is calculated over many plays. The game display is not representative of any physical device and is for illustrative purposes only. Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser. TM and © 2026 Stake Engine.") continue;
|
|
239
|
+
const social = toSocial(s);
|
|
240
|
+
if (social !== s) socialEn[s] = social;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
menu,
|
|
244
|
+
socialEn
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
export { DEFAULT_CONTROL_GUIDE, SOCIAL_REPLACEMENTS, STAKE_DISCLAIMER, buildRules, findRestricted, toSocial };
|
|
249
|
+
|
|
250
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","names":[],"sources":["../src/rules/social.ts","../src/rules/index.ts"],"sourcesContent":["// The canonical Stake SOCIAL/sweepstakes wording dictionary. In social mode, restricted\n// gambling phrases must be swapped for the alternatives below. `toSocial` auto-rewrites an\n// English string (word-safe, longest-phrase-first, case-preserving) so a game rarely\n// hand-authors social copy; `findRestricted` AGGRESSIVELY scans for restricted terms — even\n// as PART of a word (paytable, betting…) — for the launch/build compliance gate.\n\n/** Restricted → replacement, ORDERED longest-phrase-first (so multi-word phrases win). */\nexport const SOCIAL_REPLACEMENTS: ReadonlyArray<readonly [string, string]> = [\n [\"be awarded to player's accounts\", \"appear in player's accounts\"],\n ['place your bets', 'come and play / join in the game'],\n ['at the cost of', 'for'],\n ['bonus buy', 'bonus / feature'],\n ['buy bonus', 'get bonus'],\n ['win feature', 'play feature'],\n ['total bet', 'total play'],\n ['pays out', 'won'],\n ['paid out', 'win'],\n ['pay out', 'win'],\n ['cost of', 'can be played for'],\n ['paytable', 'prize table'],\n ['payouts', 'wins'],\n ['payout', 'win'],\n ['betting', 'playing'],\n ['gambling', 'playing'],\n ['wagering', 'playing'],\n ['paying', 'awarding'],\n ['buying', 'playing'],\n ['buys', 'plays'],\n ['deposit', 'get coins'],\n ['withdraw', 'redeem'],\n ['purchase', 'play'],\n ['currency', 'token'],\n ['credits', 'coins'],\n ['credit', 'coins'],\n ['winnings', 'prizes'],\n ['payer', 'winner'],\n ['bought', 'instantly triggered'],\n ['gamble', 'play'],\n ['wager', 'play'],\n ['rebet', 'respin'],\n ['stakes', 'play amounts'],\n ['stake', 'play amount'],\n ['money', 'coins'],\n ['cash', 'coins'],\n ['bets', 'plays'],\n ['bet', 'play'],\n ['pays', 'wins'],\n ['paid', 'won'],\n ['pay', 'win'],\n ['buy', 'play'],\n];\n\nconst escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\nconst matchCase = (matched: string, repl: string): string =>\n matched[0] && matched[0] === matched[0].toUpperCase() && matched[0] !== matched[0].toLowerCase()\n ? repl.charAt(0).toUpperCase() + repl.slice(1)\n : repl;\n\n// Word-boundary regexes (so \"bet\"→\"play\" never breaks \"better\"/\"internet\"). Built once.\nconst REPLACERS = SOCIAL_REPLACEMENTS.map(([term, repl]) => [new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'gi'), repl] as const);\n// Aggressive substring matchers for the CHECK (catch \"pay\" inside \"paytable\", etc.).\nconst RESTRICTED = SOCIAL_REPLACEMENTS.map(([term]) => term);\n\n/**\n * Rewrite an English string into its social/sweepstakes wording — restricted phrases swapped\n * for their alternatives (word-boundary safe, longest-phrase-first, case-preserving). Idempotent\n * enough for authoring: run it over your menu/feature copy to auto-generate social variants.\n */\nexport function toSocial(text: string): string {\n let out = text;\n for (const [re, repl] of REPLACERS) out = out.replace(re, (m) => matchCase(m, repl));\n return out;\n}\n\n/**\n * AGGRESSIVELY find restricted phrases in an English string — matches even as part of a word\n * (so `paytable`, `betting`, `payout` are all flagged). Returns the offending terms. Use it at\n * launch/build to fail on any social-copy that would still surface gambling wording.\n */\nexport function findRestricted(text: string): string[] {\n const lower = text.toLowerCase();\n const hits: string[] = [];\n for (const term of RESTRICTED) if (lower.includes(term)) hits.push(term);\n return hits;\n}\n","// @stakeplate/core/rules — `buildRules` assembles a Stake-COMPLIANT rules section (an\n// @open-slot-ui `MenuSpec`) so every game ships the mandatory copy without re-authoring it:\n// a **User Interaction Guide** (a description of every standard control), the **Game info**\n// grid (RTP / volatility / max win) and the **EXACT Stake Engine disclaimer** — authored ONCE\n// here. The game supplies only its own about/paytable/features. The library's white HTML menu\n// (open-slot-ui) renders it; the built-in Settings (Sound · Language · Quick spin) are the\n// menu's own — a game NEVER invents settings.\n//\n// Social wording: `buildRules` returns `socialEn` — { normal → social } for the strings it\n// authored that carry restricted terms (bet/pay…). Merge into `config.socialMessages.en`. The\n// disclaimer is legal-exact — mandated verbatim on every platform, so it is NEVER swapped.\n\nimport type { BlockSpec, MenuSpec } from '@open-slot-ui/core';\nimport { toSocial } from './social';\n\nexport * from './social';\n\n/** RTP / volatility / max-win etc. — shown as a stat grid. Values are display strings. */\nexport interface RulesStats {\n rtp?: string; // \"96.00%\"\n volatility?: string; // \"High\"\n maxWin?: string; // \"5,000×\"\n lines?: string; // \"5\"\n /** Any extra label → value rows (e.g. per-mode RTP/Max win). */\n extra?: Array<{ label: string; value: string }>;\n}\n\nexport interface BuildRulesOptions {\n /** How Settings exposes sound: a `'toggle'` only · `'master'` slider · `'sliders'`\n * (Music + Effects). Default `'sliders'`. */\n sound?: 'toggle' | 'master' | 'sliders';\n /** An \"About the game\" intro paragraph. */\n about?: string;\n /** \"How to play\" steps. */\n howToPlay?: string[];\n /** Feature cards (Wild / Scatter / Bonus …). */\n features?: Array<{ icon?: string; title: string; text?: string }>;\n /** The game's paytable blocks (usually one `{ kind: 'paytable' }`). */\n paytable?: BlockSpec[];\n /** RTP / volatility / max-win — the Game info grid. Must match the certified report. */\n stats?: RulesStats;\n /** Extra rules blocks appended before the disclaimer. */\n extra?: BlockSpec[];\n /** Override the built-in per-control guide lines (each: `**Name** — what it does.`). */\n controlGuide?: string[];\n /** Include the Stake disclaimer (default `true`). */\n disclaimer?: boolean;\n}\n\nexport interface BuiltRules {\n menu: MenuSpec;\n /** { normal → social } for the strings buildRules authored — merge into `config.socialMessages.en`. */\n socialEn: Record<string, string>;\n}\n\n// ── Canonical, core-authored copy ───────────────────────────────────────────────────\n\n/** Every interactive control described (Stake: the Info/Help must explain each button). */\nexport const DEFAULT_CONTROL_GUIDE: string[] = [\n '**Spin** — plays one round at your current bet; press and hold to spin in turbo.',\n '**+ / −** — raise or lower your bet before spinning.',\n '**Autoplay** — plays a set number of rounds automatically; tap again to stop any time.',\n '**Turbo / Quick spin** — speeds up spins by shortening the animation; the result is identical.',\n '**Buy Feature** — instantly buys entry to the bonus for the shown price (needs confirmation).',\n '**Menu** — opens settings, the paytable and these rules.',\n '**Sound** — mutes or unmutes all game audio.',\n];\n\n/** The EXACT Stake Engine disclaimer — never reworded, never given a social override. */\nexport const STAKE_DISCLAIMER =\n 'Malfunction voids all wins and plays. A consistent internet connection is required. In the event of a disconnection, reload the game to finish any uncompleted rounds. The expected return is calculated over many plays. The game display is not representative of any physical device and is for illustrative purposes only. Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser. TM and © 2026 Stake Engine.';\n\n/** Collect every translatable English string in a block tree (for social auto-derivation). */\nfunction collectStrings(blocks: BlockSpec[], out: string[] = []): string[] {\n for (const b of blocks) {\n switch (b.kind) {\n case 'text':\n case 'heading':\n case 'subheading':\n case 'legal':\n out.push(b.text);\n break;\n case 'callout':\n if (b.title) out.push(b.title);\n out.push(b.text);\n break;\n case 'steps':\n out.push(...b.items);\n break;\n case 'stat-grid':\n for (const it of b.items) out.push(it.label, it.value);\n break;\n case 'cards':\n for (const it of b.items) { out.push(it.title); if (it.text) out.push(it.text); }\n break;\n case 'table':\n if (b.columns) out.push(...b.columns);\n for (const r of b.rows) out.push(...r);\n break;\n case 'media':\n if (b.title) out.push(b.title);\n out.push(b.text);\n break;\n case 'paytable':\n for (const r of b.rows) if (r.symbol) out.push(r.symbol);\n break;\n case 'group':\n if (b.title) out.push(b.title);\n collectStrings(b.children, out);\n break;\n default:\n break;\n }\n }\n return out;\n}\n\n// ── The builder ────────────────────────────────────────────────────────────────────\n\nexport function buildRules(opts: BuildRulesOptions = {}): BuiltRules {\n const rules: BlockSpec[] = [];\n\n if (opts.about) {\n rules.push({ kind: 'heading', id: 'r-about-h', text: 'About the game' });\n rules.push({ kind: 'text', id: 'r-about', text: opts.about });\n }\n\n if (opts.howToPlay?.length) {\n rules.push({ kind: 'heading', id: 'r-play-h', text: 'How to play' });\n rules.push({ kind: 'steps', id: 'r-play', ordered: true, items: opts.howToPlay });\n }\n\n if (opts.features?.length) {\n rules.push({ kind: 'heading', id: 'r-feat-h', text: 'Features' });\n rules.push({ kind: 'cards', id: 'r-feat', items: opts.features });\n }\n\n // ── User Interaction Guide (Stake compliance: a description for EVERY control) ──\n rules.push({ kind: 'heading', id: 'r-ctrl-h', text: 'Controls' });\n rules.push({ kind: 'text', id: 'r-ctrl-0', text: 'Every interactive button and what it does:' });\n rules.push({ kind: 'steps', id: 'r-ctrl', ordered: false, items: opts.controlGuide ?? DEFAULT_CONTROL_GUIDE });\n\n // ── Game info (RTP / volatility / max win — must match the certified report) ──\n const stats = opts.stats;\n if (stats) {\n const items: Array<{ label: string; value: string }> = [];\n if (stats.rtp) items.push({ label: 'RTP', value: stats.rtp });\n if (stats.volatility) items.push({ label: 'Volatility', value: stats.volatility });\n if (stats.maxWin) items.push({ label: 'Max win', value: stats.maxWin });\n if (stats.lines) items.push({ label: 'Lines', value: stats.lines });\n if (stats.extra) items.push(...stats.extra);\n if (items.length) {\n rules.push({ kind: 'heading', id: 'r-info-h', text: 'Game info' });\n rules.push({ kind: 'stat-grid', id: 'r-info', items });\n }\n }\n\n if (opts.extra?.length) rules.push(...opts.extra);\n\n // ── The exact Stake disclaimer ──\n if (opts.disclaimer !== false) {\n rules.push({ kind: 'heading', id: 'r-general-h', text: 'General' });\n rules.push({ kind: 'text', id: 'r-general', text: STAKE_DISCLAIMER });\n }\n\n const menu: MenuSpec = {\n ...(opts.sound ? { sound: opts.sound } : {}),\n ...(opts.paytable ? { paytable: opts.paytable } : {}),\n rules,\n };\n\n // Auto-derive the social copy: scan EVERY English string in the menu and swap restricted\n // wording (`toSocial`). The disclaimer is legal-exact (mandated verbatim) → never rewritten.\n const socialEn: Record<string, string> = {};\n for (const s of collectStrings([...rules, ...(opts.paytable ?? [])])) {\n if (s === STAKE_DISCLAIMER) continue;\n const social = toSocial(s);\n if (social !== s) socialEn[s] = social;\n }\n return { menu, socialEn };\n}\n"],"mappings":"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"}
|
package/dist/stores.d.ts
ADDED
package/dist/stores.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { makeAutoObservable } from "mobx";
|
|
2
|
+
//#region src/stores/index.ts
|
|
3
|
+
/** Player funds + the current bet. Mutated only through actions. */
|
|
4
|
+
var BalanceStore = class {
|
|
5
|
+
balance = 0;
|
|
6
|
+
/** Base bet (per line/spin), before any mode cost multiplier. */
|
|
7
|
+
bet = 0;
|
|
8
|
+
lastWin = 0;
|
|
9
|
+
constructor() {
|
|
10
|
+
makeAutoObservable(this);
|
|
11
|
+
}
|
|
12
|
+
setBalance(v) {
|
|
13
|
+
this.balance = v;
|
|
14
|
+
}
|
|
15
|
+
setBet(v) {
|
|
16
|
+
this.bet = v;
|
|
17
|
+
}
|
|
18
|
+
/** Take the stake at spin press (win is credited on settle). */
|
|
19
|
+
debitStake(stake) {
|
|
20
|
+
this.balance -= stake;
|
|
21
|
+
}
|
|
22
|
+
/** Undo a debit (the round never happened). */
|
|
23
|
+
refund(stake) {
|
|
24
|
+
this.balance += stake;
|
|
25
|
+
}
|
|
26
|
+
/** Record + credit a settled win. */
|
|
27
|
+
credit(win) {
|
|
28
|
+
this.lastWin = win;
|
|
29
|
+
this.balance += win;
|
|
30
|
+
}
|
|
31
|
+
/** Apply the AUTHORITATIVE post-round balance + record the win (server-settled). */
|
|
32
|
+
settle(balance, win) {
|
|
33
|
+
this.balance = balance;
|
|
34
|
+
this.lastWin = win;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** Session facts from `/wallet/authenticate` (read-mostly). */
|
|
38
|
+
var SessionStore = class {
|
|
39
|
+
sessionId = "";
|
|
40
|
+
currency = "USD";
|
|
41
|
+
rtp = 96;
|
|
42
|
+
availableBets = [];
|
|
43
|
+
jurisdiction = {};
|
|
44
|
+
constructor() {
|
|
45
|
+
makeAutoObservable(this);
|
|
46
|
+
}
|
|
47
|
+
set(patch) {
|
|
48
|
+
Object.assign(this, patch);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/** Player-facing UI/session state. Games extend with their own store for feature flags. */
|
|
52
|
+
var UiStore = class {
|
|
53
|
+
spinning = false;
|
|
54
|
+
/** Free spins remaining (0 = normal play). */
|
|
55
|
+
freeSpins = 0;
|
|
56
|
+
/** A one-shot mode for the next spin (e.g. a bought bonus) — wins over `activeMode`. */
|
|
57
|
+
oneShotMode = null;
|
|
58
|
+
/** The persistent active mode (e.g. a toggled boost). */
|
|
59
|
+
activeMode = null;
|
|
60
|
+
/** Replay mode (Stake `replay=true`). */
|
|
61
|
+
replay = false;
|
|
62
|
+
constructor() {
|
|
63
|
+
makeAutoObservable(this);
|
|
64
|
+
}
|
|
65
|
+
setSpinning(v) {
|
|
66
|
+
this.spinning = v;
|
|
67
|
+
}
|
|
68
|
+
setFreeSpins(n) {
|
|
69
|
+
this.freeSpins = Math.max(0, n);
|
|
70
|
+
}
|
|
71
|
+
setOneShotMode(m) {
|
|
72
|
+
this.oneShotMode = m;
|
|
73
|
+
}
|
|
74
|
+
setActiveMode(m) {
|
|
75
|
+
this.activeMode = m;
|
|
76
|
+
}
|
|
77
|
+
/** The mode for the next spin: one-shot wins, then the active mode, then 'base'. */
|
|
78
|
+
nextMode() {
|
|
79
|
+
const m = this.oneShotMode ?? this.activeMode ?? "base";
|
|
80
|
+
this.oneShotMode = null;
|
|
81
|
+
return m;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** Aggregates the base stores. A game may compose this with its own stores. */
|
|
85
|
+
var RootStore = class {
|
|
86
|
+
balance = new BalanceStore();
|
|
87
|
+
session = new SessionStore();
|
|
88
|
+
ui = new UiStore();
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { BalanceStore, RootStore, SessionStore, UiStore };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=stores.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stores.js","names":[],"sources":["../src/stores/index.ts"],"sourcesContent":["// Base observable state (MobX). Balance/Session/UI are game-agnostic; a game adds its\n// own stores (its round model, feature state) alongside these via the DI it owns. All\n// amounts are MAJOR units (the transport handles API-unit conversion).\n\nimport { makeAutoObservable } from 'mobx';\nimport type { JurisdictionConfig } from '../rgs/protocol';\n\n/** Player funds + the current bet. Mutated only through actions. */\nexport class BalanceStore {\n balance = 0;\n /** Base bet (per line/spin), before any mode cost multiplier. */\n bet = 0;\n lastWin = 0;\n\n constructor() {\n makeAutoObservable(this);\n }\n\n setBalance(v: number): void {\n this.balance = v;\n }\n setBet(v: number): void {\n this.bet = v;\n }\n /** Take the stake at spin press (win is credited on settle). */\n debitStake(stake: number): void {\n this.balance -= stake;\n }\n /** Undo a debit (the round never happened). */\n refund(stake: number): void {\n this.balance += stake;\n }\n /** Record + credit a settled win. */\n credit(win: number): void {\n this.lastWin = win;\n this.balance += win;\n }\n /** Apply the AUTHORITATIVE post-round balance + record the win (server-settled). */\n settle(balance: number, win: number): void {\n this.balance = balance;\n this.lastWin = win;\n }\n}\n\n/** Session facts from `/wallet/authenticate` (read-mostly). */\nexport class SessionStore {\n sessionId = '';\n currency = 'USD';\n rtp = 96;\n availableBets: number[] = [];\n jurisdiction: JurisdictionConfig = {};\n\n constructor() {\n makeAutoObservable(this);\n }\n\n set(patch: Partial<SessionStore>): void {\n Object.assign(this, patch);\n }\n}\n\n/** Player-facing UI/session state. Games extend with their own store for feature flags. */\nexport class UiStore {\n spinning = false;\n /** Free spins remaining (0 = normal play). */\n freeSpins = 0;\n /** A one-shot mode for the next spin (e.g. a bought bonus) — wins over `activeMode`. */\n oneShotMode: string | null = null;\n /** The persistent active mode (e.g. a toggled boost). */\n activeMode: string | null = null;\n /** Replay mode (Stake `replay=true`). */\n replay = false;\n\n constructor() {\n makeAutoObservable(this);\n }\n\n setSpinning(v: boolean): void {\n this.spinning = v;\n }\n setFreeSpins(n: number): void {\n this.freeSpins = Math.max(0, n);\n }\n setOneShotMode(m: string | null): void {\n this.oneShotMode = m;\n }\n setActiveMode(m: string | null): void {\n this.activeMode = m;\n }\n /** The mode for the next spin: one-shot wins, then the active mode, then 'base'. */\n nextMode(): string {\n const m = this.oneShotMode ?? this.activeMode ?? 'base';\n this.oneShotMode = null;\n return m;\n }\n}\n\n/** Aggregates the base stores. A game may compose this with its own stores. */\nexport class RootStore {\n readonly balance = new BalanceStore();\n readonly session = new SessionStore();\n readonly ui = new UiStore();\n}\n"],"mappings":";;;AAQA,IAAa,eAAb,MAA0B;CACxB,UAAU;;CAEV,MAAM;CACN,UAAU;CAEV,cAAc;EACZ,mBAAmB,IAAI;CACzB;CAEA,WAAW,GAAiB;EAC1B,KAAK,UAAU;CACjB;CACA,OAAO,GAAiB;EACtB,KAAK,MAAM;CACb;;CAEA,WAAW,OAAqB;EAC9B,KAAK,WAAW;CAClB;;CAEA,OAAO,OAAqB;EAC1B,KAAK,WAAW;CAClB;;CAEA,OAAO,KAAmB;EACxB,KAAK,UAAU;EACf,KAAK,WAAW;CAClB;;CAEA,OAAO,SAAiB,KAAmB;EACzC,KAAK,UAAU;EACf,KAAK,UAAU;CACjB;AACF;;AAGA,IAAa,eAAb,MAA0B;CACxB,YAAY;CACZ,WAAW;CACX,MAAM;CACN,gBAA0B,CAAC;CAC3B,eAAmC,CAAC;CAEpC,cAAc;EACZ,mBAAmB,IAAI;CACzB;CAEA,IAAI,OAAoC;EACtC,OAAO,OAAO,MAAM,KAAK;CAC3B;AACF;;AAGA,IAAa,UAAb,MAAqB;CACnB,WAAW;;CAEX,YAAY;;CAEZ,cAA6B;;CAE7B,aAA4B;;CAE5B,SAAS;CAET,cAAc;EACZ,mBAAmB,IAAI;CACzB;CAEA,YAAY,GAAkB;EAC5B,KAAK,WAAW;CAClB;CACA,aAAa,GAAiB;EAC5B,KAAK,YAAY,KAAK,IAAI,GAAG,CAAC;CAChC;CACA,eAAe,GAAwB;EACrC,KAAK,cAAc;CACrB;CACA,cAAc,GAAwB;EACpC,KAAK,aAAa;CACpB;;CAEA,WAAmB;EACjB,MAAM,IAAI,KAAK,eAAe,KAAK,cAAc;EACjD,KAAK,cAAc;EACnB,OAAO;CACT;AACF;;AAGA,IAAa,YAAb,MAAuB;CACrB,UAAmB,IAAI,aAAa;CACpC,UAAmB,IAAI,aAAa;CACpC,KAAc,IAAI,QAAQ;AAC5B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { a as MockOptions, i as MockNetworkManager, o as ScriptedRound } from "./network-pBW8CX1o.js";
|
|
2
|
+
import { r as InstantTicker, t as HudPort } from "./hud-port-DNfotn4U.js";
|
|
3
|
+
//#region src/testing/index.d.ts
|
|
4
|
+
/** A recording no-op HudPort — inspect `.calls` in tests. */
|
|
5
|
+
interface RecordingHud extends HudPort {
|
|
6
|
+
readonly calls: Array<{
|
|
7
|
+
m: string;
|
|
8
|
+
args: unknown[];
|
|
9
|
+
}>;
|
|
10
|
+
reportedRounds(): Array<{
|
|
11
|
+
win: number;
|
|
12
|
+
bet: number;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
declare function mockHud(): RecordingHud;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { InstantTicker, MockNetworkManager, type MockOptions, RecordingHud, type ScriptedRound, mockHud };
|
|
18
|
+
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { t as MockNetworkManager } from "./MockNetworkManager-cj8WT5v0.js";
|
|
2
|
+
import { t as InstantTicker } from "./ticker-A-XgayZv.js";
|
|
3
|
+
//#region src/testing/index.ts
|
|
4
|
+
function mockHud() {
|
|
5
|
+
const calls = [];
|
|
6
|
+
const rec = (m) => (...args) => {
|
|
7
|
+
calls.push({
|
|
8
|
+
m,
|
|
9
|
+
args
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
calls,
|
|
14
|
+
setBalance: rec("setBalance"),
|
|
15
|
+
setBet: rec("setBet"),
|
|
16
|
+
setTotalWin: rec("setTotalWin"),
|
|
17
|
+
setFreeSpins: rec("setFreeSpins"),
|
|
18
|
+
reportRound: rec("reportRound"),
|
|
19
|
+
showRgsError: rec("showRgsError"),
|
|
20
|
+
showError: rec("showError"),
|
|
21
|
+
setReplay: rec("setReplay"),
|
|
22
|
+
replayStart: rec("replayStart"),
|
|
23
|
+
replayEnd: rec("replayEnd"),
|
|
24
|
+
lockInput: rec("lockInput"),
|
|
25
|
+
unlockInput: rec("unlockInput"),
|
|
26
|
+
on: (_type, _fn) => () => {},
|
|
27
|
+
reportedRounds() {
|
|
28
|
+
return calls.filter((c) => c.m === "reportRound").map((c) => ({
|
|
29
|
+
win: c.args[0],
|
|
30
|
+
bet: c.args[1]
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { InstantTicker, MockNetworkManager, mockHud };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/testing/index.ts"],"sourcesContent":["// @stakeplate/core/testing — the dev kit. The InstantTicker + MockNetworkManager (both\n// re-exported) let a whole round run headless + deterministically; `mockHud()` is a\n// no-op HudPort that records calls so tests can assert on what the engine drove.\n\nimport type { HudPort, ReplayInfo } from '../engine/hud-port';\n\nexport { InstantTicker } from '../engine/ticker';\nexport { MockNetworkManager } from '../rgs/MockNetworkManager';\nexport type { ScriptedRound, MockOptions } from '../rgs/MockNetworkManager';\n\n/** A recording no-op HudPort — inspect `.calls` in tests. */\nexport interface RecordingHud extends HudPort {\n readonly calls: Array<{ m: string; args: unknown[] }>;\n reportedRounds(): Array<{ win: number; bet: number }>;\n}\n\nexport function mockHud(): RecordingHud {\n const calls: Array<{ m: string; args: unknown[] }> = [];\n const rec = (m: string) => (...args: unknown[]) => {\n calls.push({ m, args });\n };\n return {\n calls,\n setBalance: rec('setBalance'),\n setBet: rec('setBet'),\n setTotalWin: rec('setTotalWin'),\n setFreeSpins: rec('setFreeSpins'),\n reportRound: rec('reportRound'),\n showRgsError: rec('showRgsError'),\n showError: rec('showError'),\n setReplay: rec('setReplay'),\n replayStart: rec('replayStart') as (info: ReplayInfo, onPlay?: () => void) => void,\n replayEnd: rec('replayEnd') as (info: ReplayInfo, onReplay: () => void) => void,\n lockInput: rec('lockInput'),\n unlockInput: rec('unlockInput'),\n on: (_type: string, _fn: (p: unknown) => void) => () => {},\n reportedRounds() {\n return calls.filter((c) => c.m === 'reportRound').map((c) => ({ win: c.args[0] as number, bet: c.args[1] as number }));\n },\n };\n}\n"],"mappings":";;;AAgBA,SAAgB,UAAwB;CACtC,MAAM,QAA+C,CAAC;CACtD,MAAM,OAAO,OAAe,GAAG,SAAoB;EACjD,MAAM,KAAK;GAAE;GAAG;EAAK,CAAC;CACxB;CACA,OAAO;EACL;EACA,YAAY,IAAI,YAAY;EAC5B,QAAQ,IAAI,QAAQ;EACpB,aAAa,IAAI,aAAa;EAC9B,cAAc,IAAI,cAAc;EAChC,aAAa,IAAI,aAAa;EAC9B,cAAc,IAAI,cAAc;EAChC,WAAW,IAAI,WAAW;EAC1B,WAAW,IAAI,WAAW;EAC1B,aAAa,IAAI,aAAa;EAC9B,WAAW,IAAI,WAAW;EAC1B,WAAW,IAAI,WAAW;EAC1B,aAAa,IAAI,aAAa;EAC9B,KAAK,OAAe,cAAoC,CAAC;EACzD,iBAAiB;GACf,OAAO,MAAM,QAAQ,MAAM,EAAE,MAAM,aAAa,CAAC,CAAC,KAAK,OAAO;IAAE,KAAK,EAAE,KAAK;IAAc,KAAK,EAAE,KAAK;GAAa,EAAE;EACvH;CACF;AACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/engine/ticker.ts
|
|
2
|
+
/** Real timer — `setTimeout`-backed, `performance.now()` clock. */
|
|
3
|
+
var RealTicker = class {
|
|
4
|
+
now() {
|
|
5
|
+
return (typeof performance !== "undefined" ? performance.now() : Date.now()) / 1e3;
|
|
6
|
+
}
|
|
7
|
+
delay(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** Instant ticker for tests — every delay resolves immediately; the clock advances by
|
|
12
|
+
* the requested amount so `now()` deltas + `minimumRoundDuration` maths stay sane. */
|
|
13
|
+
var InstantTicker = class {
|
|
14
|
+
t = 0;
|
|
15
|
+
now() {
|
|
16
|
+
return this.t;
|
|
17
|
+
}
|
|
18
|
+
delay(ms) {
|
|
19
|
+
this.t += Math.max(0, ms) / 1e3;
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { RealTicker as n, InstantTicker as t };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=ticker-A-XgayZv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticker-A-XgayZv.js","names":[],"sources":["../src/engine/ticker.ts"],"sourcesContent":["// The timing seam. Prod uses real wall-clock delays; tests use an INSTANT ticker so\n// a whole round runs headless with no rAF/setTimeout. Phases `await ctx.ticker.delay(ms)`\n// for beats + `minimumRoundDuration`; game scenes drive their own animation ticker.\n\nexport interface Ticker {\n /** Seconds since some epoch (monotonic-ish). */\n now(): number;\n /** Resolve after `ms` milliseconds. */\n delay(ms: number): Promise<void>;\n}\n\n/** Real timer — `setTimeout`-backed, `performance.now()` clock. */\nexport class RealTicker implements Ticker {\n now(): number {\n return (typeof performance !== 'undefined' ? performance.now() : Date.now()) / 1000;\n }\n delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));\n }\n}\n\n/** Instant ticker for tests — every delay resolves immediately; the clock advances by\n * the requested amount so `now()` deltas + `minimumRoundDuration` maths stay sane. */\nexport class InstantTicker implements Ticker {\n private t = 0;\n now(): number {\n return this.t;\n }\n delay(ms: number): Promise<void> {\n this.t += Math.max(0, ms) / 1000;\n return Promise.resolve();\n }\n}\n"],"mappings":";;AAYA,IAAa,aAAb,MAA0C;CACxC,MAAc;EACZ,QAAQ,OAAO,gBAAgB,cAAc,YAAY,IAAI,IAAI,KAAK,IAAI,KAAK;CACjF;CACA,MAAM,IAA2B;EAC/B,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,KAAK,IAAI,GAAG,EAAE,CAAC,CAAC;CACtE;AACF;;;AAIA,IAAa,gBAAb,MAA6C;CAC3C,IAAY;CACZ,MAAc;EACZ,OAAO,KAAK;CACd;CACA,MAAM,IAA2B;EAC/B,KAAK,KAAK,KAAK,IAAI,GAAG,EAAE,IAAI;EAC5B,OAAO,QAAQ,QAAQ;CACzB;AACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stakeplate/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Batteries-included Stake Engine game core — RGS transport, boot, round FSM, HUD binding, audio, i18n and a compliant rules builder. Bring a scene, a Present phase and a pure interpretBook; the core does the rest.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"stake",
|
|
7
|
+
"stake-engine",
|
|
8
|
+
"slot",
|
|
9
|
+
"slots",
|
|
10
|
+
"igaming",
|
|
11
|
+
"rgs",
|
|
12
|
+
"pixijs",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "schmooky and the stakeplate contributors",
|
|
17
|
+
"homepage": "https://github.com/schmooky/stakeplate#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/schmooky/stakeplate.git",
|
|
21
|
+
"directory": "packages/core"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/schmooky/stakeplate/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"source": "./src/index.ts",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"source": "./src/index.ts",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./rgs": {
|
|
39
|
+
"source": "./src/rgs/index.ts",
|
|
40
|
+
"types": "./dist/rgs.d.ts",
|
|
41
|
+
"import": "./dist/rgs.js",
|
|
42
|
+
"default": "./dist/rgs.js"
|
|
43
|
+
},
|
|
44
|
+
"./stores": {
|
|
45
|
+
"source": "./src/stores/index.ts",
|
|
46
|
+
"types": "./dist/stores.d.ts",
|
|
47
|
+
"import": "./dist/stores.js",
|
|
48
|
+
"default": "./dist/stores.js"
|
|
49
|
+
},
|
|
50
|
+
"./audio": {
|
|
51
|
+
"source": "./src/audio/index.ts",
|
|
52
|
+
"types": "./dist/audio.d.ts",
|
|
53
|
+
"import": "./dist/audio.js",
|
|
54
|
+
"default": "./dist/audio.js"
|
|
55
|
+
},
|
|
56
|
+
"./testing": {
|
|
57
|
+
"source": "./src/testing/index.ts",
|
|
58
|
+
"types": "./dist/testing.d.ts",
|
|
59
|
+
"import": "./dist/testing.js",
|
|
60
|
+
"default": "./dist/testing.js"
|
|
61
|
+
},
|
|
62
|
+
"./rules": {
|
|
63
|
+
"source": "./src/rules/index.ts",
|
|
64
|
+
"types": "./dist/rules.d.ts",
|
|
65
|
+
"import": "./dist/rules.js",
|
|
66
|
+
"default": "./dist/rules.js"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"files": [
|
|
70
|
+
"dist",
|
|
71
|
+
"README.md",
|
|
72
|
+
"LICENSE"
|
|
73
|
+
],
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "tsdown",
|
|
82
|
+
"test": "vitest run",
|
|
83
|
+
"test:watch": "vitest",
|
|
84
|
+
"typecheck": "tsc --noEmit"
|
|
85
|
+
},
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"@open-slot-ui/core": ">=0.8.2",
|
|
88
|
+
"@open-slot-ui/pixi": ">=0.8.2",
|
|
89
|
+
"@schmooky/zvuk": ">=1.13",
|
|
90
|
+
"pixi.js": "^8"
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"@open-slot-ui/core": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"@open-slot-ui/pixi": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"@schmooky/zvuk": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"pixi.js": {
|
|
103
|
+
"optional": true
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"dependencies": {
|
|
107
|
+
"mobx": "^6.13.0"
|
|
108
|
+
},
|
|
109
|
+
"devDependencies": {
|
|
110
|
+
"@open-slot-ui/core": "^0.8.2",
|
|
111
|
+
"@open-slot-ui/pixi": "^0.8.2",
|
|
112
|
+
"@schmooky/zvuk": "^1.13.0",
|
|
113
|
+
"pixi.js": "^8",
|
|
114
|
+
"tsdown": "^0.22.4",
|
|
115
|
+
"typescript": "^5.7.2",
|
|
116
|
+
"vitest": "^3.0.5"
|
|
117
|
+
}
|
|
118
|
+
}
|