gacha-machine

Community@rain/gacha-machine
Community skill — not reviewed. It was shared by caramel pudding and has not been checked by Neta. Read SKILL.md below before letting an agent use it.
Use it in Cohub

Paste this into your agent's chat — it fetches the skill and follows it.

Use the skill: gacha-machine. Run `npx -y @neta-art/neta-search-cli@latest skills use @rain/gacha-machine --via site` and follow what it prints. Read its complete output, redirecting it to a temporary file first if necessary, and resolve relative paths from the skill directory it prints.
Install from a terminal
npx -y @neta-art/neta-search-cli@latest skills use @rain/gacha-machine
Summary

A tap-to-spin prize machine that reveals one item at a time until the pool runs out.

  • Tap the button, the machine shakes and lights up, then one prize you haven't won yet pops out in a spinning 3D card.
  • Each prize shows its price; tap Keep to add it to your collection and see the running total.
  • Fits one phone screen without scrolling or zooming, with a warehouse to review what you've collected.
  • Works best with a strong machine image and a themed prize set: character cards, collectible items, or event rewards.
  • You can set different prices per item or make rare prizes cost absurd amounts for dramatic effect.
SKILL.md
# Gacha Machine

A single-page prize machine: the creator taps **Spin**, the machine shakes and
flashes, one prize they have not won yet pops out, a full-screen card floats it
in 3D, and **Keep** files it into the warehouse. The pool empties; the button
goes dark.

Two modes, nothing else:

- **Build** — no machine exists yet; follow Steps 0–7 end to end.
- **Extend** — a machine is shipped; change exactly the named feature (price
  tags, SVG icons, machine scale, phone fit, a prize, a caption).

## Hard scope rule

Change ONLY what the creator asked for. Never "improve" colours, copy, prices,
or layout on the way past. If a fix genuinely needs a second change, say so and
wait. Every iteration ends with: *don't change anything I didn't ask for.*

## Out of scope

- World settings, other Works, or files outside `works/webs/<slug>/`.
- Deleting what already exists. `/public` is overwrite-only, and a retired
  versioned file stays where it is.
- Publishing the Work, or the skill, unless the creator asks in this turn.
- Real-money odds, payouts, or any framing that reads as gambling.

## Required inputs, and when to ask

| Input | Counts as given when |
| --- | --- |
| Theme and background colour | named in the message |
| Machine image | supplied, or "generate one" |
| Prize list | every prize has an image and a name |
| Prices | a default per type, plus any hand-priced item |
| Icon style | emoji, or a described SVG shape |
| Machine scale | default, or a percentage |
| Naming policy | verbatim, or the creator says to name freely |

Missing pieces → ask once, all of them in one message, before any code. All
present → build through to delivery without further questions.

## Delivery

Reply in the creator's language, and write the text they will see in the Work
(labels, README, disclaimer) in that language too — prize names stay verbatim.
Hand the finished Work over with an interaction-components card whose target is
the Work; its id comes from the `neta work publish` output.

## The five non-negotiables

1. **Names are verbatim.** If the creator says "use my image names, don't
   change them", every name in data and on screen matches the file name
   character for character — including spaces (`黑巧甜心派 明信片`), casing and
   suffixes.
2. **One screen, no scroll.** Phone layout shows header, machine, spin button,
   prize strip and disclaimer without scrolling — `100dvh`, not `100vh`.
3. **The page cannot be dragged or zoomed.** `body { position: fixed }` plus
   `touch-action: manipulation`; double-tapping Spin must not zoom.
4. **`claimed` is set only on Keep.** Opening the reveal is not winning it;
   closing the tab mid-reveal must not burn the prize.
5. **It is a toy, not a shop.** An "entertainment only, no real prizes are
   awarded" disclaimer is always on screen.

## What you are building

| Region | Contents |
| --- | --- |
| Header | music toggle, warehouse button with count badge |
| Stage | machine artwork, centred, ~83% of viewport width |
| Action | one Spin button |
| Prize strip | horizontal scroll of every prize, each with a small price tag |
| Footer | disclaimer, one or two lines |

State machine — no other states:

```
idle ──Spin──▶ drawing (1.75 s: shake + sparkle) ──▶ reveal (full-screen overlay)
                                                        │
                                              Keep ──────┴────── dismissed / tab closed
                                                │                    │
                                          warehouse +1           back to idle, prize stays
                                          claimed = true           unclaimed
                                       pool empty → Spin disabled, dimmed
```

The overlay is `position: fixed; inset: 0` on `body`, never inside the
scrolling strip or the machine wrapper.

## Step 0 — Spec round, before any code

Write a one-page PRD and confirm it. Ask in the creator's own language.

1. **Theme and background** — one colour, given by the creator.
2. **Prize pool** — per item: display name (verbatim), type, image, price.
   Group by type, and state the count per group.
3. **Prices** — one default per type, plus any hand-priced item (hidden or
   chase prizes often carry an absurd number such as `99999999999`). Format
   everywhere with thousands separators.
4. **Icon style** — emoji, or inline SVG line art. If SVG, describe the shape
   ("a backpack", "a shopping bag").
5. **Machine scale** — default, or a percentage ("30% bigger"). It must still
   fit one screen; scale the wrapper, not the pixels.
6. **Naming policy** — verbatim or free. Ask; do not assume.
7. **Phone targets** — one screen, no scroll, no drag, no double-tap zoom.

Only then build. A wrong naming or price decision discovered after the pool is
wired costs a full pass.

## Step 1 — Assets, then a contact sheet

Copy every image into `works/webs/<slug>/assets/`. **Never trust a
machine-generated description order** — the model's caption order and the file
order disagree, and that is how pictures end up on the wrong prize.

Build a numbered contact sheet and look at it with your own eyes:

```bash
mkdir -p /tmp/verify && cd <upload-dir>
n=0
for f in *.webp; do
  n=$((n+1))
  convert "$f" -background white -alpha remove -alpha off -resize 240x240 \
    /tmp/verify/$(printf "%02d" $n).png
done
# then compose rows of five and stack them (montage crashes on CJK names)
```

Read `/tmp/verify/*.png` and match each tile to its file name. Record the
mapping in the data table by the creator's original names.

## Step 2 — Compress

```bash
convert machine.webp -resize '800x800>' -strip assets/machine-min.png   # keep alpha, output PNG
convert prize.webp   -resize '560x560>' -strip assets/<original-name>-min.png
```

Keep the original files; only the `-min` set ships.

## Step 3 — Scaffold

```bash
node /mods/neta/neta work init web works/webs/<slug> --title "<Title>"
```

Three files, always separate: `index.html`, `style.css`, `app.js`. For a large
change rewrite the whole file with `write`; for a small one use `edit` with
`oldText` copied **character for character from a fresh read**. Reconstructing
`oldText` from memory fails, and one bad entry rejects the whole batch.

## Step 4 — Build

Order matters: data first, then markup, then behaviour, then motion.

- **Prize data** — one array of `{ id, name, type, image, price?, claimed }`.
  Fill missing prices from a `type → price` map at init.
- **Prices in three places** — under each strip item, in the reveal overlay,
  and on each warehouse card; the warehouse header shows the sum of claimed
  prizes.
- **Icons** — inline SVG using `stroke="currentColor"` so theme colours reach
  them. Never ship an emoji where the creator asked for line art.
- **Machine layout** — `flex: 1 1 auto; min-height: 0` on the stage so it
  shrinks; the prize strip is `flex: 0 0 auto` and is never squeezed out.
- **Audio** — a real `<audio>` element driven by its own `play`/`pause`
  events, with a `userPaused` flag so the autoplay retry does not fight the
  creator's choice.

Code for all of the above is in `references/code-patterns.md`.

## Step 5 — The three phone laws

1. `height: 100vh; height: 100dvh` on the active page, `overflow: hidden`.
   `100vh` on a phone includes the space the address bar will reclaim, so the
   real viewport cannot hold the content. Desktop tests pass anyway; assert on
   a mobile viewport.
2. `html, body` locked: `height: 100%; overflow: hidden; overscroll-behavior:
   none`, and `body { position: fixed; inset: 0 }`. Re-open only the regions
   that must move — the strip (`touch-action: pan-x`) and the warehouse page
   (`overflow-y: auto; touch-action: pan-y`). Keep a `@media (max-height:
   600px)` fallback that releases the lock instead of clipping.
3. `html, body, button { touch-action: manipulation }`, `dblclick`
   `preventDefault()`, and `user-select: none` on body. Do **not** swallow
   `touchend` within 300 ms — it eats legitimate rapid taps.

Add `interactive-widget=resizes-content` and a `viewport-fit=cover` viewport
meta, and pad with `env(safe-area-inset-*)`.

## Step 6 — Regression, on a mobile viewport

Run the checklist in `references/qa-checklist.md` before claiming anything
works. Minimum: page loads, every image has `naturalWidth > 0`, prices render
in all three places and total correctly, shake → sparkle → overlay → keep
sequence, one-screen fit at 390×844 and 375×667, body is `position: fixed`,
`visualViewport.scale === 1` after mixed taps, and no console errors (ignore
`bgm.mp3 net::ERR_ABORTED` — that is blocked autoplay, not a fault).

## Step 7 — README, then publish

Write `works/webs/<slug>/README.md` like an open-source project page: centred
title, one-line pitch, badges (pool size / version / mobile / zero build), live
demo link, two phone screenshots side by side (main screen and a reveal) in
`docs/`, feature list, how to play, prize table, mobile-fit table, quick start,
file tree, implementation notes, changelog, and a disclaimer block. Always:
art and music rights belong to their original authors, and "for entertainment
only, no real prizes" must appear.

```bash
node /mods/neta/neta work publish web works/webs/<slug>     # work card, iframe (gyro blocked inside)
```

If a public bare link is wanted, copy the runtime files to
`/public/games/<slug>/`. `public.cohub.run` caches per path for 30 days and
**ignores query strings**, so a new version means new file names
(`style.v4.css`, `app.v4.js`) with `index.html` pointing at them. `/public` is
overwrite-only: no deletes, no `sed -i`; stale files are harmless.

## Pitfalls that have actually shipped

1. **CJK file names crash `montage`.** Convert to numbered PNGs and append them
   by hand.
2. **`oldText` from memory** silently rejects a whole batch of edits. Read,
   copy, verify what landed.
3. **`?v=` does not bust the CDN.** Rename the file, then `curl` the live URL
   and grep for a new marker before reporting success.
4. **Versioned copies drift.** `style.css` and `style.v4.css` both exist; an
   edit to one without the other produces a page that ignores the change.
5. **`100vh` fakes a single screen.** Only `100dvh` plus a mobile assertion is
   proof.
6. **`overflow: hidden` alone does not stop rubber-banding.** `body` must be
   `position: fixed`.
7. **Aggressive `touchend` suppression** removes valid double taps.
8. **Overlay nested inside a transformed ancestor** misplaces the card; mount it
   on `body`.
9. **`claimed` set when the card opens** loses prizes on a refresh.
10. **Listeners attached to a "mobile-like" branch** while
    `DeviceOrientationEvent` exists but never fires: track a `gyroLive` flag
    that only a real event sets, and keep the pointer path alive until then.
11. **The shell iframe blocks the gyroscope.** 3D tilt needs the bare link.
12. **Autoplay rejection logged as a console error** is expected; filter it.
13. **Duplicate suffix variants** ("same character, two versions") need
    distinct ids and names — never overwrite one with the other.
14. **`montage`/`convert` on huge images** can exhaust memory; resize per file
    before composing.

## References

- `references/code-patterns.md` — prize data and prices, SVG icon, machine
  scaling, viewport lock, zoom lock, gyro live flag, state machine, overlay,
  warehouse, audio.
- `references/qa-checklist.md` — the ten Playwright checks with assertions and
  known false failures.
- `references/prompt-template.md` — the spec prompt to hand a creator, plus the
  iteration sentences that map to specific fixes.

## File map

```
works/webs/<slug>/            # authoritative source
├── index.html / style.css / app.js
├── style.v<N>.css / app.v<N>.js   # CDN-busting copies, changed in lockstep
├── assets/                   # machine + prizes (creator's names) + BGM
├── docs/                     # README screenshots
└── README.md
/public/games/<slug>/         # optional bare link (overwrite only)
```