PURPOSE

Cinematic module for the artifact_box reward family. Plays an alien-treasure reveal where three reward cards flip from a runed back face to their front face one at a time L→R, each landing emitting a rarity-colored rune ring burst with a chime. Despite the filename, the visual is explicitly not a glitch/horror effect — it is a cybernetic alien-artifact discovery sequence. Exports artifactGlitchModule with id 'artifact-treasure'.

OWNS

  • Module-local clock _elapsed plus _clockArmed flag, driven off ctx.dt because game.time freezes during level-up.
  • Per-card state: _flipStarted, _landed, _landedAt, and a stashed _cardRect rectangle per slot (three slots).
  • A rune particle pool _runes (capped at RUNE_POOL_CAP = 72) of RuneParticle records with position, velocity, rotation, age, life, color, and shape (hex / triangle / plus / circle-dot).
  • Painting of the back face (dark plate with cyan radial glow, pulsing central hex glyph + crosshair, rarity-accented border) via _paintBackFace / _hexPath / _roundRect.
  • Painting of the front-approx half-flip plate (rarity gradient + white accent ring) inside drawCardOverride.
  • Painting of the expanding ring pulse on each flip-land and additive lighter rune-particle rendering in drawOverlay.
  • The ambient cyan radial glow drawn behind the card row in drawBackdrop.

READS FROM

  • CinematicContext from ./registrystate, dt, W, H, choices (per-card rarity).
  • RARITY_ACCENT from ../card-theme for rarity-tinted colors.
  • isSkipRewardAnimations from ../../core/config/_accessibility for the skip-animations branch.

PUSHES TO

  • setSlotIntroDuration(...) from ../hud — sets 2.55 for the normal path so cards become interactable shortly after the last rune burst, or 0.05 when reward animations are skipped.
  • playCinematicTone(...) from ./audio — emits 'sparkle' pre-flip shimmer and 'cheer' + 'sparkle' cues per flip-land.
  • SampleSfx.playArtifactReveal() from ../../audio/sample-sfx — artifact reveal stinger on onStart.
  • Canvas via the CanvasRenderingContext2D passed to drawBackdrop, drawCardOverride, and drawOverlay.

DOES NOT

  • Does not read or modify game.time (frozen during level-up); all timing comes from accumulated ctx.dt.
  • Does not own card layout, badge, icon, name, description, or pill rendering — once _elapsed is past flipStart + FLIP_DURATION, drawCardOverride returns false and hud’s default paint takes over.
  • Does not allocate beyond RUNE_POOL_CAP rune particles; spawn loop breaks once the cap is reached.
  • Does not perform input handling, gameplay logic, or rarity selection — choices arrive prepopulated via ctx.choices.
  • Does not persist any state across cinematics; _reset is called in both onStart and onEnd.

Signals

  • One-shot cue per flip start: playCinematicTone('sparkle', 1800 + i * 120, 0.12) when _elapsed first crosses FLIP_START_TIMES[i].
  • One-shot cues per flip land: playCinematicTone('cheer', 520 + i * 55) and playCinematicTone('sparkle', 2400 + i * 140), paired with a _spawnRuneBurst at the card center.
  • Module-load-time stinger: SampleSfx.playArtifactReveal() fires in onStart for the non-skip path.

Entry points

  • onStart(_ctx) — resets all state; in skip-animations mode marks every card landed at _elapsed = 0 and sets a 0.05s slot intro; otherwise sets a 2.55s slot intro and plays the artifact reveal stinger.
  • onUpdate(ctx) — parks the clock during state === 'announce', then accumulates _elapsed += ctx.dt. Fires flip-start cues, lands cards once a _cardRect is available (otherwise retries next frame), and steps rune particles with drag Math.pow(0.90, ctx.dt * 60).
  • drawBackdrop(ctx2d, ctx) — paints the ambient cyan radial glow once past announce.
  • drawCardOverride(ctx2d, cardIndex, x, y, w, h, reward, _alpha, _scale, _isSelected, ctx) — stashes _cardRect[cardIndex], suppresses paint during announce (returns true), paints the back face pre-flip and during the first half of the flip, paints the rarity-gradient front-approx during the second half, then returns false post-flip to hand off to default paint.
  • drawOverlay(ctx2d, _ctx) — draws the expanding rarity-colored impact ring per landed card (lifetime RING_LIFE = 0.55) and additive-composited rune particles with a fade curve (p < 0.15 ? p / 0.15 : 1 - (p - 0.15) / 0.85) plus a white inner dot.
  • onEnd(_ctx) — calls _reset.
  • artifactGlitchModule: CinematicModule — exported registration, id 'artifact-treasure'.

Pattern notes

  • Tunables block at the top of the file: FLIP_START_TIMES = [0.30, 1.10, 1.90], FLIP_DURATION = 0.45, RING_LIFE = 0.55, RUNES_PER_CARD_BY_RARITY (8 / 10 / 12 / 14 / 18 for common / uncommon / rare / epic / legendary), RUNE_POOL_CAP = 72. Last flip lands at ~2.35s, slot intro sized to 2.55s so interactability arrives shortly after the last rune burst.
  • The flip is a Math.abs(Math.cos(p * Math.PI)) horizontal scale (xScale 1 → 0 at half → 1 at full). The first half paints back face, the second half paints a cheap “front-approx” rarity gradient plate so the reveal identity is visible mid-flip before hud’s default paint takes over.
  • Landing is gated on _cardRect[i] being non-null; if drawCardOverride has not stashed the rect yet, _landed[i] stays false and the land retries next frame.
  • Clock arming pattern: while ctx.state === 'announce' the elapsed clock stays parked; the first non-announce frame sets _clockArmed = true and zeroes _elapsed. This sidesteps the frozen game.time during level-up.
  • Rune burst seeding: _spawnRuneBurst uses seed = i + 1 to vary baseAngle = (seed * 51.7) % (2π), per-particle speed = 170 + ((i * 37 + seed * 11) % 130), size = 7 + ((i * 13 + seed) % 6), lifeSec = 0.6 + ((i * 23 + seed * 7) % 30) / 100, and shape (i % 4).
  • Module-scoped mutable state (single-instance cinematic) is acceptable here because cinematics run one at a time and _reset brackets each play; both onStart and onEnd invoke it.
  • Skip-animations short-circuit: isSkipRewardAnimations() returns early from every hook except onStart, which still calls setSlotIntroDuration(0.05) and marks every card landed at time 0 so downstream state machines (slot_lock / shine / showing) advance immediately.
  • Rune particles render with globalCompositeOperation = 'lighter' for an additive glow; each particle also gets a small white inner dot at size * 0.22 radius to sell the “glowing rune” feel.
  • Per-shape paths defined in _runePath: shape 0 hex outline, 1 upward triangle (s * 0.87 base), 2 plus/cross with thickness s * 0.32, 3 outer circle (caller fills inner dot separately).