Skip to content

Proposing arcana

The catalog is at 109 arcana as of year-1. New ideas are welcome, but most arcanum proposals are best discussed before code lands — duplicate behavior, balance overlap, and tone drift are common pitfalls.

Before writing code, open an issue using the arcanum-proposal label. Include:

  • Name — Aziraphale-Crowley register. “The bus ticket” not “Lucky Charm.”
  • Tier — common (~30 chip), uncommon (~60), rare (~100), mythic (~200).
  • TriggeronWin, onSurrender, onArcanum, etc. Reuse existing triggers; new triggers require engine wiring.
  • What it does — one sentence. The math should fit on one line.
  • Why this charm fills a gap — what does the catalog not already do?
  • Composition — does it interact with Long-Game / Heavy Marks / drawback negotiation / build identities?

Every shipped arcanum has a one-line behavior. The cascade is loud because there are 109 of them — each charm doing too much would be illegible.

GoodBad
”+5 chips per 7 in a winning hand""+5 chips per 7 in a winning hand, except on Tuesdays, with a multiplier that increases by…"
"×3 mult on first 3 pair-wins per campaign""Variable mult that depends on three persistent flags and the player’s deck size”

If you can’t write the behavior in one line, the charm is two charms or one too-complex charm.

TierFelt impactFire frequencyExample
Common+5-15% liftOften (most hands)bus_ticket, paper_flower
Uncommon+15-30% liftMany handsoilcloth_apron
Rare+30-60% liftSome handsthe_marked_card
Mythic+60-100%+ liftRare or cappedblack_envelope, pair_hunter

The decision-impact harness (test/sim/decision_impact_test.dart) asserts every non-vow charm produces ≥15% score lift in Monte Carlo. A new charm needs to pass this.

YesNo
The Pawn TicketLuckBoost+
The Hollow MirrorDoubleDamage
The Bone PactScary Skeleton
Stationmaster’s WhistleWhistle of +5 Multiplier

Names should evoke the dealer’s pocket or his table — small objects, old, slightly tarnished, with personal weight. The flavor line on fire is in the same register:

flavor: 'a tooth, returned to its socket.'

If your proposal is accepted, the implementation looks like:

  1. Add a class in lib/sim/arcana/arcana.dart (alphabetical-ish inside the relevant phase block).
  2. Register in the arcanumRegistry map at the bottom of that file.
  3. Unlock in MetaProgression.empty.arcanaUnlocked so it appears in the shop.
  4. Icon glyph in lib/ui/arcanum_icon.dart.
  5. Asset at assets/arcana/<id>.png. Either generate via tools/generate_charm_icons.py (Gemini) or commit manually.
  6. Tests in test/sim/<group>_test.dart covering:
    • fire conditions (when does it fire, when doesn’t it)
    • math (does the chip/mult delta match the design)
    • rate-limits if applicable
    • decision-impact (the Monte Carlo will tell you if the lift is in the right range)
  7. Bump catalog count in test/sim/arcana_test.dart:catalog has N arcana.
  8. Bump unlocked count in test/data/meta_progression_test.dart.

Drawback arcana need:

  • bool get isDrawback => true;
  • String get drawbackCost => '...'; (the constraint)
  • String get drawbackReturn => '...'; (the benefit)
  • The expectedDrawbackIds set in test/sim/drawback_marking_test.dart needs your new id.

The drawback-negotiation prompt picks up the cost/return text automatically.

Most arcana fit existing triggers (onWin, onSurrender, onResolve, etc.). If yours needs a new trigger or a new runStateFlag, the proposal review will determine whether the engine wiring is in scope.

The big “deferred” engine bits (vow_of_plunger’s any_double_allowed, strangers_half’s any_split_allowed) ship as flag-setters waiting for NightRunner integration. New drawback charms can do the same.

~92 charms × 8 chain triggers × 11 archetype builds × 15 drawback flavors = 121,440 tuple states (≥80% inert; ≥6,000 felt-distinct combos)

A new charm should add to this combinatorial space, not duplicate existing tuples. The proposal review checks for tuple overlap with the current catalog.