@energy8platform/shell 0.7.1 → 0.8.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 +1 -1
- package/dist/html.d.ts +7 -2
- package/dist/html.esm.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.esm.js +1 -1
- package/dist/pixi.cjs.js +226 -19
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +7 -2
- package/dist/pixi.esm.js +226 -19
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/bonusGroups.ts +29 -0
- package/src/core/types.ts +5 -0
- package/src/core/version.ts +1 -1
- package/src/ui/pixi/components/BuyBonus.ts +195 -19
- package/src/ui/pixi/motion-pixi.ts +16 -1
- package/src/ui/pixi/primitives/widgets.ts +9 -0
package/dist/html.cjs.js
CHANGED
|
@@ -1693,7 +1693,7 @@ class KeyboardController {
|
|
|
1693
1693
|
|
|
1694
1694
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1695
1695
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1696
|
-
const PACKAGE_VERSION = '0.
|
|
1696
|
+
const PACKAGE_VERSION = '0.8.0';
|
|
1697
1697
|
|
|
1698
1698
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1699
1699
|
function resolveConfig(config) {
|
package/dist/html.d.ts
CHANGED
|
@@ -160,6 +160,11 @@ interface BonusOption {
|
|
|
160
160
|
priceMultiplier: number;
|
|
161
161
|
/** Per-option accent override. Falls back to the type default (bonus → purple, feature → gold). */
|
|
162
162
|
accentColor?: string;
|
|
163
|
+
/** Options sharing a `groupedBy` key occupy ONE card slot and are flipped through with arrows —
|
|
164
|
+
* e.g. four same-priced Ante variants that each swap the character, its art and its volatility.
|
|
165
|
+
* Pixi shell only; the DOM shell ignores the key and keeps a card per option. Options with a
|
|
166
|
+
* `custom` renderer are never grouped. */
|
|
167
|
+
groupedBy?: string;
|
|
163
168
|
/** Override the card UI. Return the card's inner content; the shell keeps the grid wrapper,
|
|
164
169
|
* accent vars and live re-pricing, and runs the normal buy flow when you call `ctx.select()`.
|
|
165
170
|
* Core uses `unknown`; each renderer re-exports a typed alias (ui/html → HTMLElement,
|
|
@@ -631,7 +636,7 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
|
|
|
631
636
|
tokens: ShellTokens;
|
|
632
637
|
layout: ShellLayoutMode;
|
|
633
638
|
soundOn: boolean;
|
|
634
|
-
readonly engineVersion = "0.
|
|
639
|
+
readonly engineVersion = "0.8.0";
|
|
635
640
|
readonly actions: ShellActions;
|
|
636
641
|
private renderer;
|
|
637
642
|
private i18n;
|
|
@@ -776,7 +781,7 @@ interface I18n {
|
|
|
776
781
|
declare function createI18n(opts: I18nOptions): I18n;
|
|
777
782
|
|
|
778
783
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
779
|
-
declare const PACKAGE_VERSION = "0.
|
|
784
|
+
declare const PACKAGE_VERSION = "0.8.0";
|
|
780
785
|
|
|
781
786
|
/** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
|
|
782
787
|
* an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
|
package/dist/html.esm.js
CHANGED
|
@@ -1691,7 +1691,7 @@ class KeyboardController {
|
|
|
1691
1691
|
|
|
1692
1692
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1693
1693
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1694
|
-
const PACKAGE_VERSION = '0.
|
|
1694
|
+
const PACKAGE_VERSION = '0.8.0';
|
|
1695
1695
|
|
|
1696
1696
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1697
1697
|
function resolveConfig(config) {
|
package/dist/index.cjs.js
CHANGED
|
@@ -1667,7 +1667,7 @@ class KeyboardController {
|
|
|
1667
1667
|
|
|
1668
1668
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1669
1669
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1670
|
-
const PACKAGE_VERSION = '0.
|
|
1670
|
+
const PACKAGE_VERSION = '0.8.0';
|
|
1671
1671
|
|
|
1672
1672
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1673
1673
|
function resolveConfig(config) {
|
package/dist/index.d.ts
CHANGED
|
@@ -160,6 +160,11 @@ interface BonusOption {
|
|
|
160
160
|
priceMultiplier: number;
|
|
161
161
|
/** Per-option accent override. Falls back to the type default (bonus → purple, feature → gold). */
|
|
162
162
|
accentColor?: string;
|
|
163
|
+
/** Options sharing a `groupedBy` key occupy ONE card slot and are flipped through with arrows —
|
|
164
|
+
* e.g. four same-priced Ante variants that each swap the character, its art and its volatility.
|
|
165
|
+
* Pixi shell only; the DOM shell ignores the key and keeps a card per option. Options with a
|
|
166
|
+
* `custom` renderer are never grouped. */
|
|
167
|
+
groupedBy?: string;
|
|
163
168
|
/** Override the card UI. Return the card's inner content; the shell keeps the grid wrapper,
|
|
164
169
|
* accent vars and live re-pricing, and runs the normal buy flow when you call `ctx.select()`.
|
|
165
170
|
* Core uses `unknown`; each renderer re-exports a typed alias (ui/html → HTMLElement,
|
|
@@ -631,7 +636,7 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
|
|
|
631
636
|
tokens: ShellTokens;
|
|
632
637
|
layout: ShellLayoutMode;
|
|
633
638
|
soundOn: boolean;
|
|
634
|
-
readonly engineVersion = "0.
|
|
639
|
+
readonly engineVersion = "0.8.0";
|
|
635
640
|
readonly actions: ShellActions;
|
|
636
641
|
private renderer;
|
|
637
642
|
private i18n;
|
|
@@ -776,7 +781,7 @@ interface I18n {
|
|
|
776
781
|
declare function createI18n(opts: I18nOptions): I18n;
|
|
777
782
|
|
|
778
783
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
779
|
-
declare const PACKAGE_VERSION = "0.
|
|
784
|
+
declare const PACKAGE_VERSION = "0.8.0";
|
|
780
785
|
|
|
781
786
|
/** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
|
|
782
787
|
* an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
|
package/dist/index.esm.js
CHANGED
|
@@ -1665,7 +1665,7 @@ class KeyboardController {
|
|
|
1665
1665
|
|
|
1666
1666
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1667
1667
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1668
|
-
const PACKAGE_VERSION = '0.
|
|
1668
|
+
const PACKAGE_VERSION = '0.8.0';
|
|
1669
1669
|
|
|
1670
1670
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1671
1671
|
function resolveConfig(config) {
|
package/dist/pixi.cjs.js
CHANGED
|
@@ -1694,7 +1694,7 @@ class KeyboardController {
|
|
|
1694
1694
|
|
|
1695
1695
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1696
1696
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1697
|
-
const PACKAGE_VERSION = '0.
|
|
1697
|
+
const PACKAGE_VERSION = '0.8.0';
|
|
1698
1698
|
|
|
1699
1699
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1700
1700
|
function resolveConfig(config) {
|
|
@@ -2326,7 +2326,22 @@ function prefersReducedMotion() {
|
|
|
2326
2326
|
}
|
|
2327
2327
|
const easeOutCubic = (p) => 1 - Math.pow(1 - p, 3);
|
|
2328
2328
|
|
|
2329
|
-
/**
|
|
2329
|
+
/**
|
|
2330
|
+
* Tween 0→1 on the Pixi ticker. Returns a canceler. Skips to the end when motion is reduced.
|
|
2331
|
+
*
|
|
2332
|
+
* CONTRACT — when motion is reduced (or `duration <= 0`) this tween does NOT animate: it applies
|
|
2333
|
+
* the final value and calls `onComplete` SYNCHRONOUSLY, before returning. Callers depend on that
|
|
2334
|
+
* (`PixiRenderer.destroy` resolves its teardown promise from `onComplete`, and must not wait on a
|
|
2335
|
+
* ticker that may already be stopped), so it must stay synchronous.
|
|
2336
|
+
*
|
|
2337
|
+
* The consequence for callers: `onComplete` is NOT an async boundary. An `onComplete` that starts
|
|
2338
|
+
* another tween is then direct recursion with no unwind — tween → onComplete → tween → … until
|
|
2339
|
+
* `RangeError: Maximum call stack size exceeded`. That shipped once: the CTA pulse loop
|
|
2340
|
+
* (`widgets.ts` startPulse) restarted itself from `onComplete`, so every player running the OS
|
|
2341
|
+
* "reduce motion" setting crashed the instant the buy-bonus panel painted its hovered CTA. Any
|
|
2342
|
+
* looping animation must therefore check `prefersReducedMotion()` and not loop — which is also
|
|
2343
|
+
* what reduced motion is asking for.
|
|
2344
|
+
*/
|
|
2330
2345
|
function tween(ticker, opts) {
|
|
2331
2346
|
const ease = opts.ease ?? easeOutCubic;
|
|
2332
2347
|
if (prefersReducedMotion() || opts.duration <= 0) {
|
|
@@ -3298,6 +3313,15 @@ class BuyBonusBadge extends pixi_js.Container {
|
|
|
3298
3313
|
startPulse() {
|
|
3299
3314
|
if (this.pulseCancel)
|
|
3300
3315
|
return;
|
|
3316
|
+
// A pulse is exactly the kind of looping decoration "reduce motion" asks us to drop — and
|
|
3317
|
+
// under it `tween` has nothing to animate anyway: it snaps to the end and calls `onComplete`
|
|
3318
|
+
// SYNCHRONOUSLY (see its contract), which this loop would answer by starting another one, and
|
|
3319
|
+
// another, with no unwind — `RangeError: Maximum call stack size exceeded`. That was a live
|
|
3320
|
+
// Stake crash: with the OS "reduce motion" setting on, opening the buy-bonus panel painted its
|
|
3321
|
+
// hovered CTA and killed the game, so the player could never buy. Now the button simply sits
|
|
3322
|
+
// still at scale 1.
|
|
3323
|
+
if (prefersReducedMotion())
|
|
3324
|
+
return;
|
|
3301
3325
|
// both the label (anchor 0.5) and the icon (pivot centred) scale around their centre
|
|
3302
3326
|
const loop = () => {
|
|
3303
3327
|
this.pulseCancel = tween(this.ticker, {
|
|
@@ -5355,6 +5379,34 @@ function imageBox(url, w, h) {
|
|
|
5355
5379
|
return c;
|
|
5356
5380
|
}
|
|
5357
5381
|
|
|
5382
|
+
/** Collapse buy-bonus options that share a `groupedBy` key into one slot. A slot with more than one
|
|
5383
|
+
* member is rendered by the pixi shell as a single card with arrows (the DOM shell ignores the key
|
|
5384
|
+
* and keeps a card per option).
|
|
5385
|
+
*
|
|
5386
|
+
* A group takes the position of its FIRST member, so the strip order stays predictable however the
|
|
5387
|
+
* members are scattered through the array. Options with a game-supplied `custom` renderer never
|
|
5388
|
+
* join a group — the shell has no layout for their interior and could not place arrows over it. */
|
|
5389
|
+
function groupBonusSlots(bonuses) {
|
|
5390
|
+
const slots = [];
|
|
5391
|
+
const byKey = new Map();
|
|
5392
|
+
for (const bonus of bonuses) {
|
|
5393
|
+
const key = bonus.groupedBy;
|
|
5394
|
+
if (!key || bonus.custom) {
|
|
5395
|
+
slots.push([bonus]);
|
|
5396
|
+
continue;
|
|
5397
|
+
}
|
|
5398
|
+
const open = byKey.get(key);
|
|
5399
|
+
if (open) {
|
|
5400
|
+
open.push(bonus);
|
|
5401
|
+
continue;
|
|
5402
|
+
}
|
|
5403
|
+
const slot = [bonus];
|
|
5404
|
+
byKey.set(key, slot);
|
|
5405
|
+
slots.push(slot);
|
|
5406
|
+
}
|
|
5407
|
+
return slots;
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5358
5410
|
/** Below this frame height (px), a wide/landscape popout (e.g. Popout S 400×225) stacks its cards
|
|
5359
5411
|
* vertically and scrolls — like mobile — so descriptions stay readable instead of shrinking to a
|
|
5360
5412
|
* ~4px floor. Wide + taller frames (Popout L and up) keep the centred horizontal row. */
|
|
@@ -5388,6 +5440,9 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5388
5440
|
cardEntries = [];
|
|
5389
5441
|
/** Keyboard focus index into the affordable subset of cardEntries. -1 = none. */
|
|
5390
5442
|
focusIndex = -1;
|
|
5443
|
+
/** Which member each `groupedBy` slot is showing, kept across rebuilds (bet steps, resizes) so a
|
|
5444
|
+
* flipped-to variant doesn't snap back to the first one under the player. */
|
|
5445
|
+
groupIndex = new Map();
|
|
5391
5446
|
/** true when cards are stacked vertically (mobile, or a short landscape popout). */
|
|
5392
5447
|
stack = false;
|
|
5393
5448
|
// Drag-scroll state. The drag is handled on the (unmasked) overlay, not the masked strip — a mask
|
|
@@ -5506,7 +5561,9 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5506
5561
|
const top = this.headerH + 6;
|
|
5507
5562
|
const areaH = this.h - top - this.footerH - 6;
|
|
5508
5563
|
const gap = 14;
|
|
5509
|
-
|
|
5564
|
+
// Grouped options share ONE slot (a carousel card), so the width fit counts SLOTS, not options.
|
|
5565
|
+
const slots = groupBonusSlots(this.bonuses);
|
|
5566
|
+
const n = Math.max(1, slots.length);
|
|
5510
5567
|
// The whole card is laid out in `em`; pick the largest em that fits BOTH dimensions of the
|
|
5511
5568
|
// available area, so the card is always fully visible (no horizontal clip, CTA never under the
|
|
5512
5569
|
// footer). emH keeps the card height within the band between header and footer; emW makes the
|
|
@@ -5518,16 +5575,44 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5518
5575
|
: (this.w - 48 - (n - 1) * gap) / (18 * n); // row: N cards + gaps fit the frame width
|
|
5519
5576
|
const em = stack ? Math.min(12, emW) : clamp(4, Math.min(emH, emW), 12);
|
|
5520
5577
|
const cardW = Math.min(18 * em, this.w - 48);
|
|
5521
|
-
const cards =
|
|
5578
|
+
const cards = [];
|
|
5579
|
+
this.cardEntries = [];
|
|
5580
|
+
for (const slot of slots) {
|
|
5581
|
+
if (slot.length === 1) {
|
|
5582
|
+
const view = this.buildCard(slot[0], cardW, em, stack, areaH);
|
|
5583
|
+
cards.push(view);
|
|
5584
|
+
this.cardEntries.push({ view, bonus: slot[0], affordable: this.isAffordable(slot[0]) });
|
|
5585
|
+
continue;
|
|
5586
|
+
}
|
|
5587
|
+
// Carousel slot: every member is built (the slot sizes to the tallest and flipping costs
|
|
5588
|
+
// nothing), but only the current one is visible.
|
|
5589
|
+
const key = slot[0].groupedBy;
|
|
5590
|
+
// Flipping the card also moves keyboard focus onto the shown member: one cursor, not two
|
|
5591
|
+
// (otherwise the next rebuild would snap the card back to whatever the focus still pointed at).
|
|
5592
|
+
const group = new GroupCard((i) => {
|
|
5593
|
+
this.groupIndex.set(key, i);
|
|
5594
|
+
this.focusShownMember(group, i);
|
|
5595
|
+
});
|
|
5596
|
+
const members = slot.map((b, i) => this.buildCard(b, cardW, em, stack, areaH, {
|
|
5597
|
+
index: i,
|
|
5598
|
+
count: slot.length,
|
|
5599
|
+
prevLabel: `bb-nav-prev:${key}`,
|
|
5600
|
+
nextLabel: `bb-nav-next:${key}`,
|
|
5601
|
+
onStep: (dir) => {
|
|
5602
|
+
if (this.dragged)
|
|
5603
|
+
return; // a scroll gesture, not a tap
|
|
5604
|
+
group.step(dir);
|
|
5605
|
+
},
|
|
5606
|
+
}));
|
|
5607
|
+
group.setCards(members, this.groupIndex.get(key) ?? 0);
|
|
5608
|
+
cards.push(group);
|
|
5609
|
+
for (let i = 0; i < slot.length; i++) {
|
|
5610
|
+
this.cardEntries.push({ view: members[i], bonus: slot[i], affordable: this.isAffordable(slot[i]), group });
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5522
5613
|
const cardH = Math.max(...cards.map((c) => c.height));
|
|
5523
5614
|
for (const c of cards)
|
|
5524
5615
|
c.setHeight(cardH);
|
|
5525
|
-
// Rebuild card entries for keyboard navigation
|
|
5526
|
-
this.cardEntries = this.bonuses.map((b, i) => ({
|
|
5527
|
-
view: cards[i],
|
|
5528
|
-
bonus: b,
|
|
5529
|
-
affordable: this.isAffordable(b),
|
|
5530
|
-
}));
|
|
5531
5616
|
// Restore or init keyboard focus on the first affordable card
|
|
5532
5617
|
const affordable = this.cardEntries.filter((e) => e.affordable);
|
|
5533
5618
|
if (affordable.length > 0) {
|
|
@@ -5568,7 +5653,7 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5568
5653
|
}
|
|
5569
5654
|
}
|
|
5570
5655
|
// ── one card ──────────────────────────────────────────────────────────────
|
|
5571
|
-
buildCard(bonus, cardW, em, stack, areaH) {
|
|
5656
|
+
buildCard(bonus, cardW, em, stack, areaH, nav) {
|
|
5572
5657
|
const accent = effectiveAccent(bonus);
|
|
5573
5658
|
const ink = contrastText(accent);
|
|
5574
5659
|
const price = bonus.priceMultiplier * this.host.state.bet;
|
|
@@ -5604,6 +5689,7 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5604
5689
|
enabled,
|
|
5605
5690
|
ctaLabel: this.host.t(bonus.type === 'feature' ? 'Activate' : 'Buy'),
|
|
5606
5691
|
onSelect: select,
|
|
5692
|
+
nav,
|
|
5607
5693
|
});
|
|
5608
5694
|
return card;
|
|
5609
5695
|
}
|
|
@@ -5722,7 +5808,7 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5722
5808
|
// the buttons keep the card silhouette without a mask blocking their pointer events.
|
|
5723
5809
|
const half = cardW / 2;
|
|
5724
5810
|
const r = 1.3 * em;
|
|
5725
|
-
card.addChild(footerButton(this.host, this.host.t('Cancel'), 'ghost', half, ctaH, 0, y, () => this.removeConfirm(), undefined, r, 0), footerButton(this.host, this.host.t(bonus.type === 'feature' ? 'Activate' : 'Buy'), accent, half, ctaH, half, y, () => {
|
|
5811
|
+
card.addChild(footerButton(this.host, this.host.t('Cancel'), 'ghost', half, ctaH, 0, y, () => this.removeConfirm(), undefined, r, 0), labelled('bb-confirm-ok', footerButton(this.host, this.host.t(bonus.type === 'feature' ? 'Activate' : 'Buy'), accent, half, ctaH, half, y, () => {
|
|
5726
5812
|
if (!this.isAffordable(bonus))
|
|
5727
5813
|
return;
|
|
5728
5814
|
if (bonus.type === 'feature')
|
|
@@ -5730,7 +5816,7 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5730
5816
|
else
|
|
5731
5817
|
this.host.actions.selectBuyBonus(bonus.id);
|
|
5732
5818
|
this.host.closeLayer();
|
|
5733
|
-
}, ink, 0, r));
|
|
5819
|
+
}, ink, 0, r)));
|
|
5734
5820
|
card.position.set((this.w - cardW) / 2, (this.h - cardH) / 2);
|
|
5735
5821
|
layer.addChild(card);
|
|
5736
5822
|
// close X pinned to screen corner
|
|
@@ -5756,14 +5842,28 @@ class BuyBonusOverlay extends pixi_js.Container {
|
|
|
5756
5842
|
this.buildCards();
|
|
5757
5843
|
this.buildFooter();
|
|
5758
5844
|
}
|
|
5845
|
+
/** Move keyboard focus onto the member a carousel slot just flipped to. Skipped when that member
|
|
5846
|
+
* can't be bought at the current bet — focus only ever sits on affordable cards. */
|
|
5847
|
+
focusShownMember(group, index) {
|
|
5848
|
+
const card = group.cardAt(index);
|
|
5849
|
+
const at = this.cardEntries.filter((ce) => ce.affordable).findIndex((ce) => ce.group === group && ce.view === card);
|
|
5850
|
+
if (at < 0)
|
|
5851
|
+
return;
|
|
5852
|
+
this.focusIndex = at;
|
|
5853
|
+
this.applyFocusRing();
|
|
5854
|
+
}
|
|
5759
5855
|
/** Apply or clear the focus ring on affordable cards. */
|
|
5760
5856
|
applyFocusRing() {
|
|
5761
5857
|
const affordable = this.cardEntries.filter((ce) => ce.affordable);
|
|
5762
5858
|
for (let i = 0; i < affordable.length; i++) {
|
|
5763
|
-
const
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5859
|
+
const entry = affordable[i];
|
|
5860
|
+
const focused = i === this.focusIndex;
|
|
5861
|
+
// Focusing a member of a carousel slot brings it to the front — the keyboard walks options,
|
|
5862
|
+
// and the card follows.
|
|
5863
|
+
if (focused && entry.group)
|
|
5864
|
+
entry.group.showCard(entry.view);
|
|
5865
|
+
if (entry.view instanceof BonusCard)
|
|
5866
|
+
entry.view.setFocused(focused);
|
|
5767
5867
|
}
|
|
5768
5868
|
}
|
|
5769
5869
|
/** Two-phase keyboard handler.
|
|
@@ -5872,6 +5972,55 @@ class CustomCard {
|
|
|
5872
5972
|
this.height = total; // the game owns its own layout; we only record the slot height
|
|
5873
5973
|
}
|
|
5874
5974
|
}
|
|
5975
|
+
/** One slot shared by several options (`groupedBy`) — e.g. four same-priced Ante characters. Every
|
|
5976
|
+
* member card is built (the slot sizes to the tallest, and flipping is then just a visibility
|
|
5977
|
+
* swap, so the card never resizes under the player); one is visible at a time. */
|
|
5978
|
+
class GroupCard {
|
|
5979
|
+
node = new pixi_js.Container();
|
|
5980
|
+
height = 0;
|
|
5981
|
+
cards = [];
|
|
5982
|
+
index = 0;
|
|
5983
|
+
/** Reports the shown member back to the overlay, which keeps it across rebuilds. */
|
|
5984
|
+
onIndex;
|
|
5985
|
+
constructor(onIndex) {
|
|
5986
|
+
this.onIndex = onIndex;
|
|
5987
|
+
}
|
|
5988
|
+
setCards(cards, index) {
|
|
5989
|
+
this.cards = cards;
|
|
5990
|
+
for (const c of cards)
|
|
5991
|
+
this.node.addChild(c.node);
|
|
5992
|
+
this.height = Math.max(...cards.map((c) => c.height));
|
|
5993
|
+
this.show(index, false); // restoring a remembered position is not a player action
|
|
5994
|
+
}
|
|
5995
|
+
/** Flip to member `i`, wrapping around in both directions. */
|
|
5996
|
+
show(i, notify = true) {
|
|
5997
|
+
const n = this.cards.length;
|
|
5998
|
+
if (n === 0)
|
|
5999
|
+
return;
|
|
6000
|
+
this.index = ((i % n) + n) % n;
|
|
6001
|
+
for (let k = 0; k < n; k++)
|
|
6002
|
+
this.cards[k].node.visible = k === this.index;
|
|
6003
|
+
if (notify)
|
|
6004
|
+
this.onIndex(this.index);
|
|
6005
|
+
}
|
|
6006
|
+
step(dir) {
|
|
6007
|
+
this.show(this.index + dir);
|
|
6008
|
+
}
|
|
6009
|
+
cardAt(index) {
|
|
6010
|
+
return this.cards[index];
|
|
6011
|
+
}
|
|
6012
|
+
/** Bring a specific member's card to the front (keyboard focus landing on that option). */
|
|
6013
|
+
showCard(card) {
|
|
6014
|
+
const i = this.cards.findIndex((c) => c === card);
|
|
6015
|
+
if (i >= 0 && i !== this.index)
|
|
6016
|
+
this.show(i);
|
|
6017
|
+
}
|
|
6018
|
+
setHeight(total) {
|
|
6019
|
+
this.height = total;
|
|
6020
|
+
for (const c of this.cards)
|
|
6021
|
+
c.setHeight(total);
|
|
6022
|
+
}
|
|
6023
|
+
}
|
|
5875
6024
|
class BonusCard {
|
|
5876
6025
|
node = new pixi_js.Container();
|
|
5877
6026
|
height = 0;
|
|
@@ -5893,9 +6042,22 @@ class BonusCard {
|
|
|
5893
6042
|
const volColor = opts.enabled ? accent : 'rgba(255,255,255,.4)';
|
|
5894
6043
|
const top = new pixi_js.Container();
|
|
5895
6044
|
let y = 1.25 * em;
|
|
5896
|
-
|
|
6045
|
+
// With arrows the title has to clear them on both sides, or a long name wraps under the glyphs.
|
|
6046
|
+
const arrowSz = 1.5 * em;
|
|
6047
|
+
const arrowPad = 0.45 * em;
|
|
6048
|
+
const titleW = opts.nav ? innerW - 2 * (arrowSz + 2 * arrowPad) : innerW;
|
|
6049
|
+
const title = makeText(bonus.title, { size: 1.3 * em, weight: '800', color: titleColor, letterSpacing: 1.3 * em * 0.04, upper: true, align: 'center', wrapWidth: titleW });
|
|
5897
6050
|
title.position.set((cardW - title.width) / 2, y);
|
|
5898
6051
|
top.addChild(title);
|
|
6052
|
+
if (opts.nav) {
|
|
6053
|
+
const boxSz = arrowSz + 2 * arrowPad;
|
|
6054
|
+
const rowY = y + (title.height - boxSz) / 2;
|
|
6055
|
+
const prev = arrowButton(opts.nav.prevLabel, -1, arrowSz, arrowPad, accent, opts.nav.onStep);
|
|
6056
|
+
const next = arrowButton(opts.nav.nextLabel, 1, arrowSz, arrowPad, accent, opts.nav.onStep);
|
|
6057
|
+
prev.position.set(0.6 * em, rowY);
|
|
6058
|
+
next.position.set(cardW - 0.6 * em - boxSz, rowY);
|
|
6059
|
+
top.addChild(prev, next);
|
|
6060
|
+
}
|
|
5899
6061
|
y += title.height + 0.75 * em;
|
|
5900
6062
|
const thumb = thumbNode(host, bonus, accent, 6.2 * em);
|
|
5901
6063
|
thumb.position.set((cardW - 6.2 * em) / 2, y);
|
|
@@ -5905,6 +6067,13 @@ class BonusCard {
|
|
|
5905
6067
|
desc.position.set((cardW - desc.width) / 2, y);
|
|
5906
6068
|
top.addChild(desc);
|
|
5907
6069
|
y += desc.height;
|
|
6070
|
+
if (opts.nav) {
|
|
6071
|
+
y += 0.7 * em;
|
|
6072
|
+
const dots = dotRow(opts.nav.count, opts.nav.index, accent, 0.24 * em, 0.5 * em);
|
|
6073
|
+
dots.position.set((cardW - dots.width) / 2, y);
|
|
6074
|
+
top.addChild(dots);
|
|
6075
|
+
y += 0.48 * em;
|
|
6076
|
+
}
|
|
5908
6077
|
this.topH = y;
|
|
5909
6078
|
// bottom block: volatility + price
|
|
5910
6079
|
let by = 0;
|
|
@@ -5919,7 +6088,8 @@ class BonusCard {
|
|
|
5919
6088
|
this.bottomBlock.addChild(priceText);
|
|
5920
6089
|
by += priceText.height;
|
|
5921
6090
|
this.bottomH = by;
|
|
5922
|
-
this.cta = ctaButton(host, opts.ctaLabel, accent, opts.ink, cardW, this.ctaH, opts.enabled, opts.onSelect, 1.4 * em);
|
|
6091
|
+
this.cta = labelled(`bb-cta:${bonus.id}`, ctaButton(host, opts.ctaLabel, accent, opts.ink, cardW, this.ctaH, opts.enabled, opts.onSelect, 1.4 * em));
|
|
6092
|
+
this.node.label = `bb-card:${bonus.id}`;
|
|
5923
6093
|
this.node.addChild(this.bg, top, this.bottomBlock, this.cta);
|
|
5924
6094
|
if (opts.enabled) {
|
|
5925
6095
|
this.node.eventMode = 'static';
|
|
@@ -5970,6 +6140,43 @@ class BonusCard {
|
|
|
5970
6140
|
}
|
|
5971
6141
|
}
|
|
5972
6142
|
// ── shared card bits ─────────────────────────────────────────────────────────
|
|
6143
|
+
/** Tag a node so the shell (and its tests) can find it in the display tree. */
|
|
6144
|
+
function labelled(label, node) {
|
|
6145
|
+
node.label = label;
|
|
6146
|
+
return node;
|
|
6147
|
+
}
|
|
6148
|
+
/** One carousel arrow. `stopPropagation` keeps the tap off the card underneath, which would
|
|
6149
|
+
* otherwise open the confirm dialog for the member being flipped away from. */
|
|
6150
|
+
function arrowButton(label, dir, size, pad, accent, onStep) {
|
|
6151
|
+
const b = new pixi_js.Container();
|
|
6152
|
+
b.label = label;
|
|
6153
|
+
const glyph = makeIcon('chevronRight', size, '#ffffff');
|
|
6154
|
+
if (dir === -1)
|
|
6155
|
+
glyph.spin = Math.PI; // no chevronLeft in the set — the glyph spins about its centre
|
|
6156
|
+
glyph.position.set(pad, pad);
|
|
6157
|
+
b.addChild(rectHit(size + pad * 2, size + pad * 2), glyph);
|
|
6158
|
+
b.eventMode = 'static';
|
|
6159
|
+
b.cursor = 'pointer';
|
|
6160
|
+
b.on('pointerover', () => glyph.setColor(accent));
|
|
6161
|
+
b.on('pointerout', () => glyph.setColor('#ffffff'));
|
|
6162
|
+
b.on('pointertap', (e) => {
|
|
6163
|
+
e.stopPropagation();
|
|
6164
|
+
onStep(dir);
|
|
6165
|
+
});
|
|
6166
|
+
return b;
|
|
6167
|
+
}
|
|
6168
|
+
/** Position indicator for a carousel slot: one dot per member, the current one in the accent. */
|
|
6169
|
+
function dotRow(count, index, accent, r, gap) {
|
|
6170
|
+
const c = new pixi_js.Container();
|
|
6171
|
+
for (let i = 0; i < count; i++) {
|
|
6172
|
+
const dot = new pixi_js.Graphics();
|
|
6173
|
+
const on = i === index;
|
|
6174
|
+
dot.circle(r, r, r).fill(on ? accent : 'rgba(255,255,255,.28)');
|
|
6175
|
+
dot.position.set(i * (2 * r + gap), 0);
|
|
6176
|
+
c.addChild(dot);
|
|
6177
|
+
}
|
|
6178
|
+
return c;
|
|
6179
|
+
}
|
|
5973
6180
|
function thumbNode(host, bonus, accent, h) {
|
|
5974
6181
|
const c = new pixi_js.Container();
|
|
5975
6182
|
c.addChild(rectHit(h, h, 0)); // size anchor (transparent)
|