fuzzyworld

explodorama / files / src / ui / offer.ts

offer.ts

3.4 KB · history · blame

1import { isCharge, type GameState, type OfferKind } from '../sim/types'
2import { CHARGE_ICONS, POWER_UP_ICONS, spriteUrl } from '../render/sprites'
3import type { SceneKind } from '../sim/demo'
4import { startClip, stopClip } from './clip'
5import { openDialog } from './dialog'
6import { CHARGE_TEXT, changeLine, POWER_UP_CLIP, POWER_UP_TEXT } from './info'
7
8/**
9 * The gem's choice (user request, 2026-10-23): its offer of power-ups, each with what it does,
10 * laid out like the almanac. One must be picked; the dialog can't be closed any other way.
11 * Developer mode's rainbow gem offers the charges too (2026-10-24).
12 *
13 * Choosing takes two taps (user request, 2026-10-24: a tap on a card chose it at once): a tap on a
14 * card selects it, and the clip above the cards (PLAN.md §13c) plays it; **Choose** takes the one
15 * selected. The first card is selected at first.
16 */
17let chosen: ((kind: OfferKind) => void) | null = null
18let selected: OfferKind | null = null
19
20export function openOffer(s: GameState, offer: OfferKind[], choose: (kind: OfferKind) => void): void {
21 chosen = choose
22 openDialog({
23 title: 'Choose a power-up',
24 body:
25 '<div class="offer-clip"><canvas class="clip" aria-hidden="true"></canvas><img class="offer-still" alt="" draggable="false" hidden></div>' +
26 offer
27 .map((k) => {
28 const text = isCharge(k) ? CHARGE_TEXT[k] : POWER_UP_TEXT[k]
29 return (
30 `<button type="button" class="almanac-entry offer-choice" data-offer="${k}" aria-pressed="false">` +
31 `<img src="${spriteUrl(isCharge(k) ? CHARGE_ICONS[k] : POWER_UP_ICONS[k])}" alt="" draggable="false">` +
32 `<div><strong>${text.name}</strong>${changeLine(s, k)}<p>${text.text}</p></div></button>`
33 )
34 })
35 .join(''),
36 buttons: [
37 {
38 label: 'Choose',
39 kind: 'primary',
40 onClick: () => {
41 chosen = null
42 if (selected) choose(selected)
43 },
44 },
45 ],
46 required: true,
47 })
48 select(offer[0]!)
49}
50
51function nameOf(k: OfferKind): string {
52 return (isCharge(k) ? CHARGE_TEXT[k] : POWER_UP_TEXT[k]).name
53}
54
55function clipOf(k: OfferKind): SceneKind | undefined {
56 return isCharge(k) ? k : POWER_UP_CLIP[k]
57}
58
59/**
60 * Selects `k`: its card marked, the Choose button naming it, and the clip above the cards playing
61 * it; one without a clip (Heart, Coin bag: nothing to see on the board) shows its picture there,
62 * big, instead (user report, 2026-10-24: the space was left empty).
63 */
64function select(k: OfferKind): void {
65 selected = k
66 for (const card of document.querySelectorAll<HTMLElement>('#dialog-body .offer-choice')) {
67 card.setAttribute('aria-pressed', String(card.dataset.offer === k))
68 }
69 const label = document.querySelector('#dialog-buttons button span')
70 if (label) label.textContent = `Choose ${nameOf(k)}`
71 const canvas = document.querySelector<HTMLCanvasElement>('#dialog-body .offer-clip canvas.clip')
72 const still = document.querySelector<HTMLImageElement>('#dialog-body .offer-still')
73 if (!canvas || !still) return
74 const clip = clipOf(k)
75 canvas.hidden = !clip
76 still.hidden = !!clip
77 if (clip) return startClip(canvas, clip)
78 stopClip(canvas)
79 still.src = spriteUrl(isCharge(k) ? CHARGE_ICONS[k] : POWER_UP_ICONS[k])
80}
81
82const dialogBody = document.getElementById('dialog-body')!
83dialogBody.addEventListener('click', (e) => {
84 const card = (e.target as Element).closest<HTMLElement>('[data-offer]')
85 if (card && chosen) select(card.dataset.offer as OfferKind)
86})