@energy8platform/shell 0.7.2 → 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 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.7.2';
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.7.2";
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.7.2";
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.7.2';
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.7.2';
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.7.2";
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.7.2";
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.7.2';
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.7.2';
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) {
@@ -5379,6 +5379,34 @@ function imageBox(url, w, h) {
5379
5379
  return c;
5380
5380
  }
5381
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
+
5382
5410
  /** Below this frame height (px), a wide/landscape popout (e.g. Popout S 400×225) stacks its cards
5383
5411
  * vertically and scrolls — like mobile — so descriptions stay readable instead of shrinking to a
5384
5412
  * ~4px floor. Wide + taller frames (Popout L and up) keep the centred horizontal row. */
@@ -5412,6 +5440,9 @@ class BuyBonusOverlay extends pixi_js.Container {
5412
5440
  cardEntries = [];
5413
5441
  /** Keyboard focus index into the affordable subset of cardEntries. -1 = none. */
5414
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();
5415
5446
  /** true when cards are stacked vertically (mobile, or a short landscape popout). */
5416
5447
  stack = false;
5417
5448
  // Drag-scroll state. The drag is handled on the (unmasked) overlay, not the masked strip — a mask
@@ -5530,7 +5561,9 @@ class BuyBonusOverlay extends pixi_js.Container {
5530
5561
  const top = this.headerH + 6;
5531
5562
  const areaH = this.h - top - this.footerH - 6;
5532
5563
  const gap = 14;
5533
- const n = Math.max(1, this.bonuses.length);
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);
5534
5567
  // The whole card is laid out in `em`; pick the largest em that fits BOTH dimensions of the
5535
5568
  // available area, so the card is always fully visible (no horizontal clip, CTA never under the
5536
5569
  // footer). emH keeps the card height within the band between header and footer; emW makes the
@@ -5542,16 +5575,44 @@ class BuyBonusOverlay extends pixi_js.Container {
5542
5575
  : (this.w - 48 - (n - 1) * gap) / (18 * n); // row: N cards + gaps fit the frame width
5543
5576
  const em = stack ? Math.min(12, emW) : clamp(4, Math.min(emH, emW), 12);
5544
5577
  const cardW = Math.min(18 * em, this.w - 48);
5545
- const cards = this.bonuses.map((b) => this.buildCard(b, cardW, em, stack, areaH));
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
+ }
5546
5613
  const cardH = Math.max(...cards.map((c) => c.height));
5547
5614
  for (const c of cards)
5548
5615
  c.setHeight(cardH);
5549
- // Rebuild card entries for keyboard navigation
5550
- this.cardEntries = this.bonuses.map((b, i) => ({
5551
- view: cards[i],
5552
- bonus: b,
5553
- affordable: this.isAffordable(b),
5554
- }));
5555
5616
  // Restore or init keyboard focus on the first affordable card
5556
5617
  const affordable = this.cardEntries.filter((e) => e.affordable);
5557
5618
  if (affordable.length > 0) {
@@ -5592,7 +5653,7 @@ class BuyBonusOverlay extends pixi_js.Container {
5592
5653
  }
5593
5654
  }
5594
5655
  // ── one card ──────────────────────────────────────────────────────────────
5595
- buildCard(bonus, cardW, em, stack, areaH) {
5656
+ buildCard(bonus, cardW, em, stack, areaH, nav) {
5596
5657
  const accent = effectiveAccent(bonus);
5597
5658
  const ink = contrastText(accent);
5598
5659
  const price = bonus.priceMultiplier * this.host.state.bet;
@@ -5628,6 +5689,7 @@ class BuyBonusOverlay extends pixi_js.Container {
5628
5689
  enabled,
5629
5690
  ctaLabel: this.host.t(bonus.type === 'feature' ? 'Activate' : 'Buy'),
5630
5691
  onSelect: select,
5692
+ nav,
5631
5693
  });
5632
5694
  return card;
5633
5695
  }
@@ -5746,7 +5808,7 @@ class BuyBonusOverlay extends pixi_js.Container {
5746
5808
  // the buttons keep the card silhouette without a mask blocking their pointer events.
5747
5809
  const half = cardW / 2;
5748
5810
  const r = 1.3 * em;
5749
- 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, () => {
5750
5812
  if (!this.isAffordable(bonus))
5751
5813
  return;
5752
5814
  if (bonus.type === 'feature')
@@ -5754,7 +5816,7 @@ class BuyBonusOverlay extends pixi_js.Container {
5754
5816
  else
5755
5817
  this.host.actions.selectBuyBonus(bonus.id);
5756
5818
  this.host.closeLayer();
5757
- }, ink, 0, r));
5819
+ }, ink, 0, r)));
5758
5820
  card.position.set((this.w - cardW) / 2, (this.h - cardH) / 2);
5759
5821
  layer.addChild(card);
5760
5822
  // close X pinned to screen corner
@@ -5780,14 +5842,28 @@ class BuyBonusOverlay extends pixi_js.Container {
5780
5842
  this.buildCards();
5781
5843
  this.buildFooter();
5782
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
+ }
5783
5855
  /** Apply or clear the focus ring on affordable cards. */
5784
5856
  applyFocusRing() {
5785
5857
  const affordable = this.cardEntries.filter((ce) => ce.affordable);
5786
5858
  for (let i = 0; i < affordable.length; i++) {
5787
- const view = affordable[i].view;
5788
- if (view instanceof BonusCard) {
5789
- view.setFocused(i === this.focusIndex);
5790
- }
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);
5791
5867
  }
5792
5868
  }
5793
5869
  /** Two-phase keyboard handler.
@@ -5896,6 +5972,55 @@ class CustomCard {
5896
5972
  this.height = total; // the game owns its own layout; we only record the slot height
5897
5973
  }
5898
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
+ }
5899
6024
  class BonusCard {
5900
6025
  node = new pixi_js.Container();
5901
6026
  height = 0;
@@ -5917,9 +6042,22 @@ class BonusCard {
5917
6042
  const volColor = opts.enabled ? accent : 'rgba(255,255,255,.4)';
5918
6043
  const top = new pixi_js.Container();
5919
6044
  let y = 1.25 * em;
5920
- const title = makeText(bonus.title, { size: 1.3 * em, weight: '800', color: titleColor, letterSpacing: 1.3 * em * 0.04, upper: true, align: 'center', wrapWidth: innerW });
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 });
5921
6050
  title.position.set((cardW - title.width) / 2, y);
5922
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
+ }
5923
6061
  y += title.height + 0.75 * em;
5924
6062
  const thumb = thumbNode(host, bonus, accent, 6.2 * em);
5925
6063
  thumb.position.set((cardW - 6.2 * em) / 2, y);
@@ -5929,6 +6067,13 @@ class BonusCard {
5929
6067
  desc.position.set((cardW - desc.width) / 2, y);
5930
6068
  top.addChild(desc);
5931
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
+ }
5932
6077
  this.topH = y;
5933
6078
  // bottom block: volatility + price
5934
6079
  let by = 0;
@@ -5943,7 +6088,8 @@ class BonusCard {
5943
6088
  this.bottomBlock.addChild(priceText);
5944
6089
  by += priceText.height;
5945
6090
  this.bottomH = by;
5946
- 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}`;
5947
6093
  this.node.addChild(this.bg, top, this.bottomBlock, this.cta);
5948
6094
  if (opts.enabled) {
5949
6095
  this.node.eventMode = 'static';
@@ -5994,6 +6140,43 @@ class BonusCard {
5994
6140
  }
5995
6141
  }
5996
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
+ }
5997
6180
  function thumbNode(host, bonus, accent, h) {
5998
6181
  const c = new pixi_js.Container();
5999
6182
  c.addChild(rectHit(h, h, 0)); // size anchor (transparent)