# Bellwake: our own cooperative living-card-game campaign, written for Tabletwin (no existing game's
# names, cards, texts or scenarios). Patterns: a campaign of three linked scenarios (a subflow per
# scenario, a setup action with reset, a wrap-up, a crossroads choice and a camp where experience
# buys upgrades); a campaign log kept as cards whose effects shape every later scenario; trauma for
# a hero who fell; player decks, hands and a resource pool; allies, gear and songs played by running
# the card's own effect (exec); an encounter deck with enemies, locations and omens (keywords Surge
# and Ambush); a doom track that rises every round and pulls enemies down from the staging area;
# objectives with progress and explorable locations; defending and attacking; two priority windows
# for fast songs; triggers and modifiers on cards (heroes, locations, enemies, the boss).
# yaml-language-server: $schema=https://tabletwin.com/models/docs/schema.json
game: bellwake
label: Bellwake
players: "2 players, together"
num_players: 2
mode: cooperative
summary: >
  The bells of Tollhaven keep the Hush away: a silent fog that eats names and voices. Two keepers,
  Hale Brann the Bellwarden and Sefa Wren the Cantor, play three scenarios in a row: climb the
  Lower Stair, relight the Foundry Below and ring the great bell on the Crown of Silence. Every
  round they gain echo to play allies, gear and songs, send characters on the venture against the
  dread of the staging area, and fight the Hush-born that come down as the Hush rises. Toll the
  bell to push the Hush back. What happens in one scenario is written in the campaign log and
  changes the next; experience buys upgrades, a fallen hero carries trauma. Silence the Unsung on
  the Crown to win.
scope: >
  Original game. One hero per player with a fixed sixteen-card deck (upgrades add cards); no
  mulligan, and a player deck that runs out is not reshuffled. Each round reveals two encounter
  cards; enemies engage when the Hush reaches their approach value, the player with fewer engaged
  enemies first. Engaged enemies attack in the order they engaged; there are no shadow cards. A
  scenario is lost when the Hush reaches 17, when both heroes have fallen, or after round 10. A
  hero who falls leaves the scenario, the player plays on with allies (and gains only 1 echo a
  round), and their enemies turn to the other hero. The campaign goes on after a lost scenario; it is won by winning the third.

constants:
  HUSH_MAX: 17
  ROUND_LIMIT: 10
  START_HAND: 5
  ECHO_PER_ROUND: 3
  TOLL_COST: 2
  WIN_XP: 2
  TRAUMA_DAMAGE: 2
  REVEALS: 2
  HEROES: [hale, sefa]
  DECKS:
    - [[stair_guard, 3], [watch_novice, 2], [tower_captain, 2], [bell_hammer, 2], [quilted_mantle, 2], [hold_fast, 2], [counter_peal, 2], [ring_the_alarm, 1]]
    - [[pilgrim, 3], [choir_scholar, 2], [wayfinder, 1], [tuning_fork, 2], [echo_stone, 1], [hymn_of_clarity, 3], [mending_chant, 2], [name_the_nameless, 2]]
  SCENARIOS:
    - id: lower_stair
      label: "1 · The Lower Stair"
      hush: 2
      objectives: [the_climb, the_gate]
      setup: [frostbitten_steps]
      encounter: [[hollow_hound, 3], [hushed_novice, 2], [silent_brother, 2], [mist_weaver, 1], [frostbitten_steps, 1], [rope_bridge, 2], [cold_refectory, 1], [creeping_silence, 2], [numbing_cold, 2], [forgotten_path, 1]]
      win_log: gate_held
      loss_log: gate_fell
    - id: foundry_below
      label: "2 · The Foundry Below"
      hush: 3
      objectives: [the_bellows, the_pour]
      setup: [furnace_mouth]
      encounter: [[hollow_hound, 2], [bell_thief, 2], [bronze_husk, 1], [silent_brother, 1], [mist_weaver, 1], [furnace_mouth, 1], [casting_pit, 2], [hall_of_still_bells, 1], [rope_bridge, 1], [creeping_silence, 1], [echoes_fade, 2], [numbing_cold, 1]]
      win_log: null
      loss_log: bell_cracked
    - id: crown_of_silence
      label: "3 · The Crown of Silence"
      hush: 4
      objectives: [the_spire, the_great_bell]
      setup: [frostbitten_steps]
      encounter: [[hollow_hound, 2], [silent_brother, 1], [mist_weaver, 2], [bronze_husk, 1], [hushed_novice, 1], [hall_of_still_bells, 1], [rope_bridge, 2], [cold_refectory, 1], [frostbitten_steps, 1], [creeping_silence, 2], [the_quiet_spreads, 2], [forgotten_path, 1]]
      win_log: null
      loss_log: null

functions:
  hero_of:
    text: "the hero of player q, or None once silenced"
    params: [q]
    x: "([c for c in party[q] if c['type'] == 'hero'] + [None])[0]"
  ready_chars:
    params: [q]
    x: "[c for c in party[q] if not c.get('exhausted')]"
  song_target:
    text: "an enemy a song may hurt (not an Unyielding one)"
    params: [e]
    x: "e['type'] == 'enemy' and 'Unyielding' not in e.get('keywords', '')"
  can_play:
    text: "a card player q may play in the muster phase: muster timing, affordable, and with a target if it needs one"
    params: [c, q]
    x: >-
      c['timing'] == 'muster' and c['cost'] <= echo[q]
      and (c['type'] != 'gear' or len(party[q]) > 0)
      and (c.get('needs') != 'damaged' or any(x.get('damage', 0) > 0 for r in players for x in party[r]))
      and (c.get('needs') != 'enemy' or any(song_target(e) for e in enemies_all))
  targets:
    text: "what card c, played by player q, is aimed at: a character for gear, a hurt character, an enemy; else nothing"
    params: [c, q]
    x: >-
      (list(party[q]) if c['type'] == 'gear'
       else [x for r in players for x in party[r] if x.get('damage', 0) > 0] if c.get('needs') == 'damaged'
       else [e for e in enemies_all if song_target(e)] if c.get('needs') == 'enemy'
       else [])
  can_sing:
    text: "a fast song player q may sing in window w"
    params: [c, q, w]
    x: >-
      c['type'] == 'song' and c['timing'] == w and c['cost'] <= echo[q]
      and (c.get('needs') != 'defender' or defender is not None)
      and (c.get('needs') != 'foe' or (foe is not None and foe in engaged[p] and song_target(foe)))
  fewest_engaged:
    text: "the standing player with the fewest engaged enemies (the first player on a tie)"
    x: "sorted((len(engaged[r]), 0 if r == first_player else 1, r) for r in standing)[0][2]"

state:
  zones:
    # campaign: kept from one scenario to the next
    campaign_log: {kind: set, owner: global, visibility: public, persist: campaign, note: "log entries; each one's effect runs at the start of every later scenario"}
    # one scenario
    deck: {kind: stack, owner: player, visibility: hidden, shuffled: true, init_x: "[[content['cards'][c] for c, n in DECKS[q] for i in range(n)] for q in players]"}
    hand: {kind: set, owner: player, visibility: owner}
    party: {kind: set, owner: player, visibility: public, init_x: "[[content['heroes'][HEROES[q]]] for q in players]", note: "the hero and the allies in play"}
    gear: {kind: set, owner: player, visibility: public, note: "gear on a character; host = its uid"}
    discard: {kind: stack, owner: player, visibility: public}
    engaged: {kind: set, owner: player, visibility: public, note: "enemies engaged with this player, in the order they engaged"}
    encounter_deck: {kind: stack, owner: global, visibility: hidden, shuffled: true, init_x: "[content['encounter'][c] for c, n in SCENARIOS[stage]['encounter'] for i in range(n)]"}
    revealed: {kind: slot, owner: global, visibility: public}
    staging: {kind: set, owner: global, visibility: public, note: "enemies and locations not yet engaged or travelled to"}
    active_location: {kind: slot, owner: global, visibility: public}
    encounter_discard: {kind: stack, owner: global, visibility: public}
    victory: {kind: set, owner: global, visibility: public, note: "silenced enemies and explored locations with victory points"}
    silenced: {kind: set, owner: global, visibility: public, note: "heroes who fell this scenario"}
  vars:
    # the campaign
    stage: {scope: global, type: int, init: 0, persist: campaign, note: "index of the scenario being played"}
    won: {scope: global, type: int, init: 0, persist: campaign, note: "scenarios won"}
    finale_won: {scope: global, type: bool, init: false, persist: campaign}
    xp: {scope: player, type: int, init: 0, persist: campaign}
    upgrades: {scope: player, type: list, init_x: "[[] for q in players]", persist: campaign}
    trauma: {scope: player, type: int, init: 0, persist: campaign, note: "each trauma: the hero starts a scenario with 2 damage"}
    # one scenario
    hush: {scope: global, type: int, init_x: "SCENARIOS[stage]['hush']", note: "the doom track; the scenario is lost at HUSH_MAX"}
    toll_cost: {scope: global, type: int, init_x: "TOLL_COST"}
    boss_bonus: {scope: global, type: int, init: 0}
    boss_down: {scope: global, type: bool, init: false}
    obj_index: {scope: global, type: int, init: 0}
    objective: {scope: global, type: ref, init_x: "SCENARIOS[stage]['objectives'][0]"}
    progress: {scope: global, type: int, init: 0}
    round_no: {scope: global, type: int, init: 1}
    first_player: {scope: global, type: ref, init: 0}
    echo: {scope: player, type: int, init: 0, note: "resources to pay for cards and the bell"}
    tolled: {scope: global, type: bool, init: false, note: "the bell rings once per round"}
    p: {scope: global, type: ref, init: 0, note: "loop variable: the player acting"}
    foe: {scope: global, type: ref, init: null, note: "loop variable: the enemy attacking or attacked"}
    defender: {scope: global, type: ref, init: null}
    playing: {scope: global, type: ref, init: null, note: "the card being played (for players choosing its target)"}
    muster_done: {scope: global, type: bool, init: false}
    camp_done: {scope: global, type: bool, init: false}
    song_resolve: {scope: global, type: int, init: 0, note: "resolve added by songs to this venture"}
    song_ward: {scope: global, type: int, init: 0, note: "ward added by songs to this attack"}
    to_reveal: {scope: global, type: int, init: 0}
    to_place: {scope: global, type: int, init: 0}
  derived:
    standing: {text: "players whose hero still stands", x: "[q for q in players if any(c['type'] == 'hero' for c in party[q])]"}
    enemies_all: {text: "enemies in the staging area or engaged", x: "[e for e in staging if e['type'] == 'enemy'] + [e for q in players for e in engaged[q]]"}
    dread_total: {text: "Σ dread in the staging area", x: "sum(max(0, modified('dread', c['dread'], card=c)) for c in staging)"}
    venture_resolve: {text: "Σ resolve of the committed characters, plus songs", x: "sum(max(0, modified('resolve', c['resolve'], card=c)) for q in players for c in party[q] if c.get('committed')) + song_resolve"}
    goal: {text: "progress the current objective needs", x: "content['objectives'][objective]['goal']"}
    final_stage: {text: "the scenario's last objective is in play", x: "obj_index == len(SCENARIOS[stage]['objectives']) - 1"}

priority:
  protocol:
    first: the first player
    first_x: first_player
    closes: all_pass_in_succession
    resolution: immediate
  windows:
    venture: {label: "Songs during the venture", allows: [fast]}
    clash: {label: "Songs during an attack", allows: [fast]}

flow:
  id: campaign
  label: Campaign
  kind: loop
  until: "all three scenarios played"
  until_x: "stage >= len(SCENARIOS)"
  children:
    - {id: play_scenario, label: Scenario, kind: subflow, ref: scenario}
    - {id: wrap_up, label: "Wrap-up: experience, trauma, campaign log", kind: step, do: wrap_up}
    - id: interlude
      label: Between scenarios
      kind: branch
      cases:
        - when: "the Foundry was won: choose what to save"
          when_x: "stage == 2 and results.get('scenario') == 'win'"
          children:
            - {id: crossroads, label: "Recast the bell or rescue the founder", kind: step, actor_x: "0", do: [recast_bell, rescue_founder]}
    - id: camp
      label: Camp
      kind: branch
      cases:
        - when: "another scenario follows"
          when_x: "stage < len(SCENARIOS)"
          children:
            - id: camp_players
              label: Each keeper spends experience
              kind: loop
              over_x: players
              as: p
              once: true
              children:
                - {id: open_camp, label: "Open the camp", kind: step, do: open_camp}
                - id: training
                  label: Learn until done
                  kind: loop
                  until: "the keeper stops or has no experience left"
                  until_x: camp_done
                  children:
                    - {id: train, label: "Learn an upgrade or keep the experience", kind: step, actor_x: p, do: [learn, keep_xp]}

flows:
  scenario:
    id: scenario
    label: Scenario
    kind: sequence
    children:
      - {id: setup, label: "Set up: reset, campaign log, upgrades, hands, first objective", kind: step, do: setup}
      - id: rounds
        label: Round
        kind: loop
        note: "until a scenario end condition fires"
        children:
          - {id: kindle, label: "1. Kindle: echo and a card", kind: step, do: kindle}
          - id: muster
            label: "2. Muster"
            kind: loop
            over_x: "[first_player, 1 - first_player]"
            as: p
            once: true
            children:
              - {id: open_muster, label: "Start the muster", kind: step, do: open_muster}
              - id: muster_turns
                label: Play cards until done
                kind: loop
                until: "this keeper has stopped"
                until_x: muster_done
                children:
                  - {id: muster_play, label: "Play a card, toll the bell, or stop", kind: step, actor_x: p, do: [play_card, toll, end_muster]}
          - id: venture
            label: "3. Venture"
            kind: sequence
            children:
              - id: commit_round
                label: Commit
                kind: loop
                over_x: "[first_player, 1 - first_player]"
                as: p
                once: true
                children:
                  - {id: commit, label: "Send ready characters (exhaust)", kind: step, actor_x: p, do: commit}
              - {id: reveal, label: "Reveal two encounter cards", kind: step, do: reveal_cards, window: venture}
              - {id: resolve, label: "Resolve vs dread: progress or the Hush rises", kind: step, do: resolve_venture}
          - {id: travel_step, label: "4. Travel", kind: step, actor_x: first_player, do: [travel, stay_put]}
          - id: encounter
            label: "5. Encounter"
            kind: sequence
            children:
              - id: optional_engagement
                label: Optional engagement
                kind: loop
                over_x: standing
                as: p
                once: true
                children:
                  - {id: opt_engage, label: "Engage an enemy from the staging area?", kind: step, actor_x: p, do: [engage, keep_distance]}
              - {id: engagement_checks, label: "Enemies with approach ≤ Hush engage", kind: step, do: engagement_checks}
          - id: clash
            label: "6. Clash"
            kind: sequence
            children:
              - id: enemy_attacks
                label: Enemy attacks
                kind: loop
                over_x: standing
                as: p
                once: true
                children:
                  - id: attacks_on_p
                    label: Each enemy engaged with this keeper
                    kind: loop
                    over: "enemy engaged with this keeper"
                    over_x: "list(engaged[p])"
                    as: foe
                    once: true
                    children:
                      - {id: defence, label: "Defend (exhaust) or take the blow", kind: step, actor_x: p, do: [defend, take_blow], window: clash}
                      - {id: blow, label: "Damage = strike − ward", kind: step, do: resolve_blow}
              - id: keeper_attacks
                label: Keeper attacks
                kind: loop
                over_x: players
                as: p
                once: true
                children:
                  - id: attacks_by_p
                    label: Each enemy engaged with this keeper
                    kind: loop
                    over: "enemy engaged with this keeper"
                    over_x: "list(engaged[p])"
                    as: foe
                    once: true
                    children:
                      - {id: strike_back, label: "Attack this enemy (exhaust) or hold", kind: step, actor_x: p, do: [attack, hold]}
          - {id: rest, label: "7. Rest: ready, the Hush rises", kind: step, do: rest}

actions:
  setup:
    effect:
      # everything without persist starts fresh: decks, hands, parties, Hush, objective
      - {op: reset, scope: scenario}
      - {op: let, bind: s, value_x: "SCENARIOS[stage]"}
      - op: for_each
        items_x: "s['setup']"
        as: sid
        body:
          - {op: move, what_x: "[c for c in encounter_deck if c['id'] == sid][0]", from: encounter_deck, to: staging}
      - {op: shuffle, zone: encounter_deck}
      # the campaign log: every entry's effect shapes this scenario
      - op: for_each
        items_x: "list(campaign_log)"
        as: entry
        body: [{op: exec, of_x: entry, content: "log_entries.*"}]
      - op: for_each
        items_x: players
        as: q
        body:
          - op: for_each
            items_x: "list(upgrades[q])"
            as: u
            body: [{op: exec, of_x: "content['upgrades'][u]", content: "upgrades.*"}]
          - {op: set, of_x: "hero_of(q)", attr: damage, value_x: "TRAUMA_DAMAGE * trauma[q]"}
          - {op: draw, from: "deck[q]", to: "hand[q]", n_x: START_HAND}
      - {op: exec, of_x: "content['objectives'][objective]", content: "objectives.*"}

  kindle:
    effect:
      - op: for_each
        items_x: players
        as: q
        body:
          - {op: add, var: "echo[q]", amount_x: "ECHO_PER_ROUND if hero_of(q) is not None else 1"}
          - {op: emit, event: kindled}
          - {op: draw, from: "deck[q]", to: "hand[q]"}

  open_muster:
    effect: [{op: set, var: muster_done, value: false}]
  play_card:
    label: "Play a card"
    pre: "a card in hand that can be played now"
    pre_x: "any(can_play(c, actor) for c in hand[actor])"
    effect:
      - {op: choose, options: "a playable card", options_x: "[c for c in hand[actor] if can_play(c, actor)]", bind: card}
      - {op: add, var: "echo[actor]", amount_x: "-card['cost']"}
      - {op: set, var: playing, value_x: card}
      - {op: let, bind: target, value: null}
      - op: if
        cond: "gear, or a song with a target: choose it"
        cond_x: "targets(card, actor)"
        then: [{op: choose, options: "a character for gear, a hurt character, or an enemy", options_x: "targets(card, actor)", bind: target}]
      # the card's own effect puts it into play (ally, gear) or does what it says (song)
      - {op: exec, of_x: "card['origin']", content: "cards.*"}
      - {op: if, cond_x: "card in hand[actor]", then: [{op: move, what_x: card, from: "hand[actor]", to: "discard[actor]"}]}
      - {op: set, var: playing, value: null}
  toll:
    label: "Toll the bell"
    pre: "once per round, for echo"
    pre_x: "not tolled and echo[actor] >= toll_cost and hush > 0"
    effect:
      - {op: add, var: "echo[actor]", amount_x: "-toll_cost"}
      - {op: set, var: tolled, value: true}
      - {op: add, var: hush, amount: -1}
      - {op: emit, event: bell_tolled}
  end_muster:
    label: "Stop playing cards"
    effect: [{op: set, var: muster_done, value: true}]

  commit:
    effect:
      - {op: choose, options: "ready characters", options_x: "ready_chars(actor)", min: 0, bind: questers}
      - op: for_each
        items_x: questers
        as: c
        body:
          - {op: set, of_x: c, attr: exhausted, value: true}
          - {op: set, of_x: c, attr: committed, value: true}
  reveal_cards:
    effect:
      - {op: set, var: to_reveal, value_x: REVEALS}
      - {op: emit, event: reveal_encounter}
  resolve_venture:
    effect:
      - {op: let, bind: r, value_x: venture_resolve}
      - {op: let, bind: d, value_x: dread_total}
      - op: if
        cond: "resolve beats dread: progress, first on the active location"
        cond_x: "r > d"
        then:
          - {op: set, var: to_place, value_x: "r - d"}
          - {op: emit, event: place_progress}
        else:
          - op: if
            cond: "dread beats resolve: the Hush rises by the difference"
            cond_x: "d > r"
            then: [{op: add, var: hush, amount_x: "modified('hush_rise', d - r)"}]
      - op: for_each
        items_x: "[c for q in players for c in party[q] if c.get('committed')]"
        as: c
        body: [{op: set, of_x: c, attr: committed, value: false}]
      - {op: set, var: song_resolve, value: 0}

  travel:
    label: "Travel to the next location"
    pre: "no active location, and a location in the staging area"
    pre_x: "not active_location and any(c['type'] == 'location' for c in staging)"
    effect:
      - {op: choose, options_x: "[c for c in staging if c['type'] == 'location']", bind: loc}
      - {op: move, what_x: loc, from: staging, to: active_location}
      - {op: emit, event: travelled}
  stay_put: {label: "Stay here", effect: []}

  engage:
    label: "Engage an enemy"
    pre_x: "any(c['type'] == 'enemy' for c in staging)"
    effect:
      - {op: choose, options_x: "[c for c in staging if c['type'] == 'enemy']", bind: e}
      - {op: move, what_x: e, from: staging, to: "engaged[actor]"}
  keep_distance: {label: "Keep your distance", effect: []}
  engagement_checks:
    effect:
      - op: repeat
        until: "no enemy in the staging area has approach ≤ Hush"
        until_x: "not standing or not [e for e in staging if e['type'] == 'enemy' and e['approach'] <= hush]"
        body:
          - {op: let, bind: e, value_x: "sorted((x['approach'], x['uid'], x) for x in staging if x['type'] == 'enemy' and x['approach'] <= hush)[0][2]"}
          - {op: let, bind: q, value_x: "fewest_engaged()"}
          - {op: move, what_x: e, from: staging, to: "engaged[q]"}

  defend:
    label: "Defend (exhaust)"
    pre_x: "foe in engaged[actor] and len(ready_chars(actor)) > 0"
    effect:
      - {op: choose, options: "a ready character", options_x: "ready_chars(actor)", bind: v}
      - {op: set, of_x: v, attr: exhausted, value: true}
      - {op: set, var: defender, value_x: v}
      - {op: set, var: song_ward, value: 0}
  take_blow:
    label: "Take the blow"
    pre_x: "foe in engaged[actor]"
    effect:
      - {op: set, var: defender, value: null}
      - {op: set, var: song_ward, value: 0}
  resolve_blow:
    pre_x: "foe in engaged[p]"
    effect:
      - {op: let, bind: power, value_x: "modified('strike', foe['strike'], card=foe)"}
      - op: if
        cond: "defended: the defender takes strike − ward"
        cond_x: "defender is not None"
        then:
          - op: if
            cond_x: "defender in party[p]"
            then: [{op: add, of_x: defender, attr: damage, amount_x: "max(0, power - defender['ward'] - song_ward)"}]
        else:
          - op: if
            cond: "undefended: the hero takes it all"
            cond_x: "hero_of(p) is not None"
            then: [{op: add, of_x: "hero_of(p)", attr: damage, amount_x: "max(0, power - song_ward)"}]
      - {op: emit, event: damage_dealt}
      - {op: emit, event: blow_resolved}
      - {op: set, var: defender, value: null}
      - {op: set, var: song_ward, value: 0}

  attack:
    label: "Attack (exhaust)"
    pre_x: "foe in engaged[actor] and any(c['strike'] > 0 for c in ready_chars(actor))"
    effect:
      - {op: choose, options: "attackers", options_x: "[c for c in ready_chars(actor) if c['strike'] > 0]", min: 1, bind: attackers}
      - op: for_each
        items_x: attackers
        as: c
        body: [{op: set, of_x: c, attr: exhausted, value: true}]
      - {op: add, of_x: foe, attr: damage, amount: "Σ strike − ward", amount_x: "max(0, sum(c['strike'] for c in attackers) - foe['ward'])"}
      - {op: emit, event: damage_dealt}
  hold: {label: "Hold", effect: []}

  # Fast songs: playable in the venture and clash windows (the card says which).
  sing:
    speed: fast
    pre: "a song for this window in hand, affordable"
    pre_x: "any(can_sing(c, actor, window) for c in hand[actor])"
    effect:
      - {op: choose, options_x: "[c for c in hand[actor] if can_sing(c, actor, window)]", bind: card}
      - {op: add, var: "echo[actor]", amount_x: "-card['cost']"}
      - {op: move, what_x: card, from: "hand[actor]", to: "discard[actor]"}
      - {op: exec, of_x: "card['origin']", content: "cards.*"}

  rest:
    effect:
      - op: for_each
        items_x: "[c for q in players for c in party[q]]"
        as: c
        body: [{op: set, of_x: c, attr: exhausted, value: false}]
      - {op: add, var: hush, amount: 1}
      - {op: set, var: first_player, value_x: "1 - first_player"}
      - {op: set, var: tolled, value: false}
      - {op: add, var: round_no, amount: 1}
      - {op: emit, event: round_end}

  wrap_up:
    effect:
      - {op: let, bind: won_it, value_x: "results.get('scenario') == 'win'"}
      - {op: let, bind: s, value_x: "SCENARIOS[stage]"}
      - op: for_each
        items_x: players
        as: q
        body:
          - {op: add, var: "xp[q]", amount: "victory points + 2 for a win", amount_x: "sum(c.get('vp', 0) for c in victory) + (WIN_XP if won_it else 0)"}
          - {op: if, cond: "the hero fell: trauma", cond_x: "hero_of(q) is None", then: [{op: add, var: "trauma[q]", amount: 1}]}
      - op: if
        cond_x: won_it
        then:
          - {op: add, var: won, amount: 1}
          - {op: if, cond_x: "s['win_log'] is not None", then: [{op: place, what_x: "content['log_entries'][s['win_log']]", to: campaign_log}]}
          - {op: if, cond_x: "stage == len(SCENARIOS) - 1", then: [{op: set, var: finale_won, value: true}]}
          - {op: emit, event: scenario_won}
        else:
          - {op: if, cond_x: "s['loss_log'] is not None", then: [{op: place, what_x: "content['log_entries'][s['loss_log']]", to: campaign_log}]}
          - {op: emit, event: scenario_lost}
      - {op: add, var: stage, amount: 1}
  recast_bell:
    label: "Recast the bell"
    effect: [{op: place, what_x: "content['log_entries']['bell_recast']", to: campaign_log}]
  rescue_founder:
    label: "Rescue the founder"
    effect: [{op: place, what_x: "content['log_entries']['founder_saved']", to: campaign_log}]
  open_camp:
    effect: [{op: set, var: camp_done, value: false}]
  learn:
    label: "Learn an upgrade"
    pre: "an upgrade you can afford and do not have yet"
    pre_x: "any(u['cost'] <= xp[actor] and u['id'] not in upgrades[actor] for u in content['upgrades'].values())"
    effect:
      - {op: choose, options_x: "[u for u in content['upgrades'].values() if u['cost'] <= xp[actor] and u['id'] not in upgrades[actor]]", bind: u}
      - {op: add, var: "xp[actor]", amount_x: "-u['cost']"}
      - {op: add, var: "upgrades[actor]", amount_x: "[u['id']]"}
  keep_xp:
    label: "Keep the experience"
    effect: [{op: set, var: camp_done, value: true}]

# Rule procedures: cards and actions call them by emitting the event.
triggers:
  - id: reveal_encounter_cards
    event: reveal_encounter
    note: "Reveal to_reveal cards: When revealed (the card's effect), Ambush, Surge; an empty deck is reshuffled and the Hush rises"
    do:
      - op: repeat
        until_x: "to_reveal <= 0"
        body:
          - {op: add, var: to_reveal, amount: -1}
          - op: if
            cond: "encounter deck empty: shuffle the discard pile back, the Hush rises"
            cond_x: "len(encounter_deck) == 0"
            then:
              - {op: move, what_x: "list(encounter_discard)", from: encounter_discard, to: encounter_deck}
              - {op: shuffle, zone: encounter_deck}
              - {op: add, var: hush, amount: 1}
          - op: if
            cond_x: "len(encounter_deck) > 0"
            then:
              - {op: draw, from: encounter_deck, to: revealed}
              - {op: let, bind: card, value_x: "revealed[-1]"}
              - {op: exec, of_x: "card['origin']", content: "encounter.*"}
              - op: if
                cond: "Ambush: engages at once"
                cond_x: "'Ambush' in card.get('keywords', '') and card in staging and standing"
                then: [{op: let, bind: q, value_x: "fewest_engaged()"}, {op: move, what_x: card, from: staging, to: "engaged[q]"}]
              - {op: if, cond_x: "card in revealed", then: [{op: move, what_x: card, from: revealed, to: encounter_discard}]}
              - {op: if, cond: "Surge: reveal one more", cond_x: "'Surge' in card.get('keywords', '')", then: [{op: add, var: to_reveal, amount: 1}]}

  - id: place_progress
    event: place_progress
    note: "to_place progress: first on the active location (explored when full), the rest on the objective (the next one begins when full)"
    do:
      - op: if
        cond_x: "active_location and to_place > 0"
        then:
          - {op: let, bind: loc, value_x: "active_location[0]"}
          - {op: let, bind: k, value_x: "min(to_place, loc['explore'] - loc.get('progress', 0))"}
          - {op: add, of_x: loc, attr: progress, amount_x: k}
          - {op: add, var: to_place, amount_x: "-k"}
          - op: if
            cond: "location explored"
            cond_x: "loc['progress'] >= loc['explore']"
            then:
              - {op: set, of_x: loc, attr: progress, value: 0}
              - op: if
                cond_x: "loc.get('vp', 0) > 0"
                then: [{op: move, what_x: loc, from: active_location, to: victory}]
                else: [{op: move, what_x: loc, from: active_location, to: encounter_discard}]
              - {op: emit, event: location_explored}
      - op: if
        cond_x: "to_place > 0"
        then:
          - {op: add, var: progress, amount_x: to_place}
          - {op: set, var: to_place, value: 0}
          - op: if
            cond: "objective complete and another follows"
            cond_x: "progress >= goal and not final_stage"
            then:
              - {op: add, var: obj_index, amount: 1}
              - {op: set, var: objective, value_x: "SCENARIOS[stage]['objectives'][obj_index]"}
              - {op: set, var: progress, value: 0}
              - {op: exec, of_x: "content['objectives'][objective]", content: "objectives.*"}

  - id: check_defeated
    event: damage_dealt
    note: "Characters and enemies whose damage reaches their health leave play; a silenced hero's enemies turn to the other hero"
    do:
      - op: for_each
        items_x: players
        as: q
        body:
          - op: for_each
            items_x: "[c for c in party[q] if c.get('damage', 0) >= c['health']]"
            as: c
            body:
              - op: if
                cond_x: "c['type'] == 'hero'"
                then:
                  - {op: move, what_x: c, from: "party[q]", to: silenced}
                  - {op: emit, event: hero_silenced}
                  - op: if
                    cond_x: "standing and engaged[q]"
                    then: [{op: move, what_x: "list(engaged[q])", from: "engaged[q]", to: "engaged[standing[0]]"}]
                else:
                  - {op: move, what_x: c, from: "party[q]", to: "discard[q]"}
              - {op: move, what_x: "[g for g in gear[q] if g.get('host') == c['uid']]", from: "gear[q]", to: "discard[q]"}
      - op: for_each
        items_x: "[e for e in enemies_all if e.get('damage', 0) >= e['health']]"
        as: e
        body:
          - op: if
            cond_x: "e in staging"
            then:
              - op: if
                cond_x: "e.get('vp', 0) > 0"
                then: [{op: move, what_x: e, from: staging, to: victory}]
                else: [{op: move, what_x: e, from: staging, to: encounter_discard}]
            else:
              - {op: let, bind: r, value_x: "[r for r in players if e in engaged[r]][0]"}
              - op: if
                cond_x: "e.get('vp', 0) > 0"
                then: [{op: move, what_x: e, from: "engaged[r]", to: victory}]
                else: [{op: move, what_x: e, from: "engaged[r]", to: encounter_discard}]
          - {op: set, of_x: e, attr: damage, value: 0}
          - {op: if, cond_x: "e['id'] == 'the_unsung'", then: [{op: set, var: boss_down, value: true}]}
          - {op: emit, event: enemy_silenced}

content:
  heroes:
    hale:
      id: hale
      label: Hale Brann
      type: hero
      tone: bronze
      resolve: 1
      strike: 3
      ward: 2
      health: 10
      status: verified
      text: "Bellwarden. After Hale defends, he deals 1 damage to the attacker."
      triggers:
        - id: hale_riposte
          event: blow_resolved
          if_x: "defender is not None and defender['id'] == self['id'] and defender in party[p] and foe in engaged[p]"
          do:
            - {op: add, of_x: foe, attr: damage, amount: 1}
            - {op: emit, event: damage_dealt}
    sefa:
      id: sefa
      label: Sefa Wren
      type: hero
      tone: silver
      resolve: 3
      strike: 1
      ward: 1
      health: 8
      status: verified
      text: "Cantor. While Sefa stands, a failed venture raises the Hush by 1 less."
      active_x: "any(c['id'] == self['id'] for q in players for c in party[q])"
      modifiers:
        - {id: sefa_steady, stat: hush_rise, change_x: "max(0, value - 1)"}

  # Player cards. Playing one runs its effect: an ally joins the party, gear goes on the character
  # chosen as target, a song does what it says. Fields: timing (muster, or the window of a fast
  # song), cost, needs (what a song is aimed at).
  cards:
    stair_guard: {id: stair_guard, label: Stair Guard, type: ally, tone: bronze, timing: muster, cost: 2, resolve: 0, strike: 2, ward: 1, health: 2, status: verified,
                  text: "Ally.", effect: &join [{op: move, what_x: card, from: "hand[actor]", to: "party[actor]"}, {op: emit, event: ally_joined}]}
    watch_novice: {id: watch_novice, label: Watch Novice, type: ally, tone: bronze, timing: muster, cost: 1, resolve: 1, strike: 1, ward: 0, health: 1, status: verified,
                   text: "Ally.", effect: *join}
    tower_captain: {id: tower_captain, label: Tower Captain, type: ally, tone: bronze, timing: muster, cost: 4, resolve: 1, strike: 3, ward: 2, health: 3, status: verified,
                    text: "Ally.", effect: *join}
    bell_hammer:
      id: bell_hammer
      label: Bell-hammer
      type: gear
      tone: bronze
      timing: muster
      cost: 1
      plus_strike: 1
      status: verified
      text: "Gear. The character gets +1 strike."
      effect: &attach
        # target: the character chosen when the gear was played
        - {op: add, of_x: target, attr: resolve, amount_x: "self.get('plus_resolve', 0)"}
        - {op: add, of_x: target, attr: strike, amount_x: "self.get('plus_strike', 0)"}
        - {op: add, of_x: target, attr: ward, amount_x: "self.get('plus_ward', 0)"}
        - {op: add, of_x: target, attr: health, amount_x: "self.get('plus_health', 0)"}
        - {op: set, of_x: card, attr: host, value_x: "target['uid']"}
        - {op: move, what_x: card, from: "hand[actor]", to: "gear[actor]"}
    quilted_mantle: {id: quilted_mantle, label: Quilted Mantle, type: gear, tone: bronze, timing: muster, cost: 1, plus_ward: 1, plus_health: 1, status: verified,
                     text: "Gear. The character gets +1 ward and +1 health.", effect: *attach}
    hold_fast:
      id: hold_fast
      label: Hold Fast
      type: song
      tone: bronze
      timing: clash
      cost: 1
      needs: defender
      status: verified
      text: "Fast, during an attack: the defender gets +2 ward."
      effect: [{op: add, var: song_ward, amount: 2}]
    counter_peal:
      id: counter_peal
      label: Counter-peal
      type: song
      tone: bronze
      timing: clash
      cost: 2
      needs: foe
      status: verified
      text: "Fast, during an attack: deal 2 damage to the attacker."
      effect:
        - {op: add, of_x: foe, attr: damage, amount: 2}
        - {op: emit, event: damage_dealt}
    ring_the_alarm:
      id: ring_the_alarm
      label: Ring the Alarm
      type: song
      tone: bronze
      timing: muster
      cost: 1
      status: verified
      text: "Draw 2 cards."
      effect: [{op: draw, from: "deck[actor]", to: "hand[actor]", n: 2}]
    pilgrim: {id: pilgrim, label: Pilgrim of the Stair, type: ally, tone: silver, timing: muster, cost: 1, resolve: 1, strike: 0, ward: 0, health: 1, status: verified,
              text: "Ally.", effect: *join}
    choir_scholar: {id: choir_scholar, label: Choir Scholar, type: ally, tone: silver, timing: muster, cost: 3, resolve: 2, strike: 1, ward: 1, health: 2, status: verified,
                    text: "Ally.", effect: *join}
    wayfinder:
      id: wayfinder
      label: Wayfinder
      type: ally
      tone: silver
      timing: muster
      cost: 2
      resolve: 1
      strike: 1
      ward: 1
      health: 2
      status: verified
      text: "Ally. When it joins: you may exhaust it to put 2 progress on the active location (overflow goes to the objective)."
      effect: *join
      triggers:
        - id: wayfinder_scout
          event: ally_joined
          optional: true
          controller_x: actor
          if_x: "card['id'] == self['id'] and active_location"
          do:
            - {op: set, of_x: card, attr: exhausted, value: true}
            - {op: set, var: to_place, value: 2}
            - {op: emit, event: place_progress}
    tuning_fork: {id: tuning_fork, label: Tuning Fork, type: gear, tone: silver, timing: muster, cost: 1, plus_resolve: 1, status: verified,
                  text: "Gear. The character gets +1 resolve.", effect: *attach}
    echo_stone:
      id: echo_stone
      label: Echo Stone
      type: gear
      tone: silver
      timing: muster
      cost: 2
      status: verified
      text: "Gear. Each kindle, its player gains 1 more echo."
      effect: *attach
      triggers:
        - id: echo_stone_kindle
          event: kindled
          if_x: "any(g['id'] == self['id'] for g in gear[q])"
          do: [{op: add, var: "echo[q]", amount_x: "len([g for g in gear[q] if g['id'] == self['id']])"}]
    hymn_of_clarity:
      id: hymn_of_clarity
      label: Hymn of Clarity
      type: song
      tone: silver
      timing: venture
      cost: 1
      status: verified
      text: "Fast, during the venture: +2 resolve."
      effect: [{op: add, var: song_resolve, amount: 2}]
    mending_chant:
      id: mending_chant
      label: Mending Chant
      type: song
      tone: silver
      timing: muster
      cost: 1
      needs: damaged
      status: verified
      text: "Heal 3 damage from a character."
      effect: [{op: set, of_x: target, attr: damage, value_x: "max(0, target['damage'] - 3)"}]
    name_the_nameless:
      id: name_the_nameless
      label: Name the Nameless
      type: song
      tone: silver
      timing: muster
      cost: 2
      needs: enemy
      status: verified
      text: "Deal 2 damage to an enemy, engaged or in the staging area."
      effect:
        - {op: add, of_x: target, attr: damage, amount: 2}
        - {op: emit, event: damage_dealt}
    # Cards that only come from the camp or the campaign log
    bell_guard_veteran: {id: bell_guard_veteran, label: Bell-guard Veteran, type: ally, tone: bronze, timing: muster, cost: 3, resolve: 1, strike: 2, ward: 2, health: 3, status: verified,
                         text: "Ally. Earned at camp.", effect: *join}
    great_canticle:
      id: great_canticle
      label: Great Canticle
      type: song
      tone: silver
      timing: venture
      cost: 2
      status: verified
      text: "Fast, during the venture: +4 resolve. Earned at camp."
      effect: [{op: add, var: song_resolve, amount: 4}]
    agna_fell: {id: agna_fell, label: "Agna Fell, Bellfounder", type: ally, tone: bronze, timing: muster, cost: 4, resolve: 2, strike: 2, ward: 2, health: 4, status: verified,
                text: "Ally. Joins from the campaign log.", effect: *join}

  # Encounter cards. Revealing one runs its effect ("when revealed"); enemies and locations go to
  # the staging area. approach: the Hush at which an enemy engages; dread: counts against the
  # venture; explore: progress a location needs; vp: victory points (experience).
  encounter:
    hollow_hound:
      {id: hollow_hound, label: Hollow Hound, type: enemy, approach: 3, dread: 1, strike: 2, ward: 0, health: 2, vp: 0, keywords: "Ambush", status: verified,
       text: "Ambush (engages at once).", effect: &to_staging [{op: move, what_x: card, from: revealed, to: staging}]}
    hushed_novice:
      id: hushed_novice
      label: Hushed Novice
      type: enemy
      approach: 5
      dread: 1
      strike: 1
      ward: 1
      health: 2
      vp: 0
      status: verified
      text: "When revealed: the Hush rises by 1."
      effect: [{op: move, what_x: card, from: revealed, to: staging}, {op: add, var: hush, amount: 1}]
    silent_brother:
      id: silent_brother
      label: Silent Brother
      type: enemy
      approach: 7
      dread: 2
      strike: 3
      ward: 1
      health: 4
      vp: 1
      status: verified
      text: "After it attacks undefended: the Hush rises by 1."
      effect: *to_staging
      triggers:
        - id: brother_undefended
          event: blow_resolved
          if_x: "defender is None and foe['id'] == self['id'] and foe in engaged[p]"
          do: [{op: add, var: hush, amount: 1}]
    mist_weaver:
      id: mist_weaver
      label: Mist Weaver
      type: enemy
      approach: 10
      dread: 2
      strike: 2
      ward: 1
      health: 3
      vp: 1
      status: verified
      text: "While in the staging area: every other enemy there has +1 dread (once, however many Mist Weavers)."
      effect: *to_staging
      active_x: "any(c['id'] == self['id'] for c in staging)"
      modifiers:
        - {id: weaver_veil, stat: dread, if_x: "card['type'] == 'enemy' and card['id'] != self['id']", change_x: "value + 1"}
    bell_thief:
      id: bell_thief
      label: Bell-thief
      type: enemy
      approach: 6
      dread: 2
      strike: 2
      ward: 0
      health: 3
      vp: 1
      status: verified
      text: "When revealed: the keeper with the most echo loses 2."
      effect:
        - {op: move, what_x: card, from: revealed, to: staging}
        - {op: let, bind: richest, value_x: "sorted((-echo[r], r) for r in players)[0][1]"}
        - {op: set, var: "echo[richest]", value_x: "max(0, echo[richest] - 2)"}
    bronze_husk:
      {id: bronze_husk, label: Bronze Husk, type: enemy, approach: 8, dread: 2, strike: 3, ward: 2, health: 5, vp: 2, keywords: "Unyielding", status: verified,
       text: "Unyielding (songs cannot hurt it).", effect: *to_staging}
    the_unsung:
      id: the_unsung
      label: The Unsung
      type: enemy
      approach: 12
      dread: 3
      strike: 4
      ward: 1
      health: 9
      vp: 5
      status: verified
      text: "Each toll of the bell deals her 2 damage, 4 once the Great Bell is complete. In the staging area: the Hush rises 1 more each rest."
      effect: *to_staging
      triggers:
        - id: unsung_tolled
          event: bell_tolled
          if_x: "any(e['id'] == self['id'] for e in enemies_all)"
          do:
            - {op: add, of_x: "[e for e in enemies_all if e['id'] == self['id']][0]", attr: damage, amount_x: "4 if objective == 'the_great_bell' and progress >= goal else 2"}
            - {op: emit, event: damage_dealt}
        - id: unsung_song
          event: round_end
          if_x: "any(e['id'] == self['id'] for e in staging)"
          do: [{op: add, var: hush, amount: 1}]
    frostbitten_steps:
      id: frostbitten_steps
      label: Frostbitten Steps
      type: location
      dread: 1
      explore: 3
      vp: 0
      status: verified
      text: "After you travel here: the Hush rises by 1."
      effect: *to_staging
      triggers:
        - id: steps_travel
          event: travelled
          if_x: "loc['id'] == self['id']"
          do: [{op: add, var: hush, amount: 1}]
    rope_bridge:
      id: rope_bridge
      label: Rope Bridge
      type: location
      dread: 2
      explore: 2
      vp: 0
      status: verified
      text: "When explored: each keeper draws 1 card."
      effect: *to_staging
      triggers:
        - id: bridge_explored
          event: location_explored
          if_x: "loc['id'] == self['id']"
          do: [{op: for_each, items_x: players, as: r, body: [{op: draw, from: "deck[r]", to: "hand[r]"}]}]
    cold_refectory:
      id: cold_refectory
      label: Cold Refectory
      type: location
      dread: 2
      explore: 3
      vp: 1
      status: verified
      text: "When explored: heal 2 damage from each hero."
      effect: *to_staging
      triggers:
        - id: refectory_explored
          event: location_explored
          if_x: "loc['id'] == self['id']"
          do:
            - op: for_each
              items_x: "[h for h in [hero_of(r) for r in players] if h is not None]"
              as: h
              body: [{op: set, of_x: h, attr: damage, value_x: "max(0, h.get('damage', 0) - 2)"}]
    furnace_mouth:
      id: furnace_mouth
      label: Furnace Mouth
      type: location
      dread: 2
      explore: 4
      vp: 1
      status: verified
      text: "While active: enemies have +1 strike."
      effect: *to_staging
      active_x: "any(c['id'] == self['id'] for c in active_location)"
      modifiers:
        - {id: furnace_heat, stat: strike, change_x: "value + 1"}
    casting_pit:
      id: casting_pit
      label: Casting Pit
      type: location
      dread: 1
      explore: 3
      vp: 0
      status: verified
      text: "When explored: each keeper gains 1 echo."
      effect: *to_staging
      triggers:
        - id: pit_explored
          event: location_explored
          if_x: "loc['id'] == self['id']"
          do: [{op: for_each, items_x: players, as: r, body: [{op: add, var: "echo[r]", amount: 1}]}]
    hall_of_still_bells:
      id: hall_of_still_bells
      label: Hall of Still Bells
      type: location
      dread: 2
      explore: 4
      vp: 2
      status: verified
      text: "While active: heroes have −1 resolve."
      effect: *to_staging
      active_x: "any(c['id'] == self['id'] for c in active_location)"
      modifiers:
        - {id: still_bells, stat: resolve, if_x: "card['type'] == 'hero'", change_x: "value - 1"}
    creeping_silence:
      {id: creeping_silence, label: Creeping Silence, type: omen, keywords: "Surge", status: verified,
       text: "Surge. When revealed: the Hush rises by 1.", effect: [{op: add, var: hush, amount: 1}]}
    numbing_cold:
      id: numbing_cold
      label: Numbing Cold
      type: omen
      status: verified
      text: "When revealed: 1 damage to each committed character."
      effect:
        - op: for_each
          items_x: "[c for r in players for c in party[r] if c.get('committed')]"
          as: c
          body: [{op: add, of_x: c, attr: damage, amount: 1}]
        - {op: emit, event: damage_dealt}
    forgotten_path:
      id: forgotten_path
      label: Forgotten Path
      type: omen
      status: verified
      text: "When revealed: remove up to 2 progress from the active location, or else from the objective."
      effect:
        - op: if
          cond_x: "active_location"
          then: [{op: set, of_x: "active_location[0]", attr: progress, value_x: "max(0, active_location[0].get('progress', 0) - 2)"}]
          else: [{op: set, var: progress, value_x: "max(0, progress - 2)"}]
    echoes_fade:
      id: echoes_fade
      label: Echoes Fade
      type: omen
      status: verified
      text: "When revealed: each keeper loses 1 echo."
      effect: [{op: for_each, items_x: players, as: r, body: [{op: set, var: "echo[r]", value_x: "max(0, echo[r] - 1)"}]}]
    the_quiet_spreads:
      id: the_quiet_spreads
      label: The Quiet Spreads
      type: omen
      status: verified
      text: "When revealed: the enemy discarded last returns to the staging area; if there is none, Surge."
      effect:
        - op: if
          cond_x: "any(c['type'] == 'enemy' for c in encounter_discard)"
          then: [{op: move, what_x: "[c for c in encounter_discard if c['type'] == 'enemy'][-1]", from: encounter_discard, to: staging}]
          else: [{op: add, var: to_reveal, amount: 1}]

  # Objectives: two per scenario; the effect runs when the objective begins.
  objectives:
    the_climb:
      {id: the_climb, label: The Climb, type: objective, stage: 1, goal: 4, status: verified,
       text: "Climb the frozen stair. When it begins: each keeper draws 1 card.",
       effect: [{op: for_each, items_x: players, as: r, body: [{op: draw, from: "deck[r]", to: "hand[r]"}]}]}
    the_gate:
      {id: the_gate, label: The Gate of Tollhaven, type: objective, stage: 2, goal: 6, status: verified,
       text: "Reach the gate before it shuts. When it begins: reveal 1 encounter card.",
       effect: [{op: set, var: to_reveal, value: 1}, {op: emit, event: reveal_encounter}]}
    the_bellows:
      {id: the_bellows, label: The Bellows, type: objective, stage: 1, goal: 5, status: verified,
       text: "Wake the old furnace. When it begins: each keeper gains 1 echo.",
       effect: [{op: for_each, items_x: players, as: r, body: [{op: add, var: "echo[r]", amount: 1}]}]}
    the_pour:
      {id: the_pour, label: The Pour, type: objective, stage: 2, goal: 7, status: verified,
       text: "Pour the bronze. When it begins: a Bronze Husk rises in the staging area.",
       effect: [{op: create, what_x: "content['encounter']['bronze_husk']", to: staging}]}
    the_spire:
      {id: the_spire, label: The Spire, type: objective, stage: 1, goal: 5, status: verified,
       text: "Climb the bell tower. When it begins: the Hush rises by 1.",
       effect: [{op: add, var: hush, amount: 1}]}
    the_great_bell:
      {id: the_great_bell, label: The Great Bell, type: objective, stage: 2, goal: 7, status: verified, boss: true,
       text: "Ring the great bell. When it begins: the Unsung enters the staging area. You win once she is silenced too.",
       effect: [{op: create, what_x: "content['encounter']['the_unsung']", to: staging, with_x: "{'health': content['encounter']['the_unsung']['health'] + boss_bonus}"}]}

  # The campaign log: entries written at the wrap-up; each effect runs at every later setup.
  log_entries:
    gate_held:
      {id: gate_held, label: The gate held, type: log, status: verified,
       text: "Each later scenario begins with the Hush 1 lower.", effect: [{op: add, var: hush, amount: -1}]}
    gate_fell:
      {id: gate_fell, label: The gate fell, type: log, status: verified,
       text: "A Silent Brother waits in the staging area of the Foundry Below.",
       effect: [{op: if, cond_x: "SCENARIOS[stage]['id'] == 'foundry_below'", then: [{op: create, what_x: "content['encounter']['silent_brother']", to: staging}]}]}
    bell_recast:
      {id: bell_recast, label: The bell was recast, type: log, status: verified,
       text: "Tolling the bell costs 1 echo less.", effect: [{op: add, var: toll_cost, amount: -1}]}
    founder_saved:
      {id: founder_saved, label: The founder was rescued, type: log, status: verified,
       text: "Agna Fell, Bellfounder, starts in the first keeper's party.", effect: [{op: create, what_x: "content['cards']['agna_fell']", to: "party[0]"}]}
    bell_cracked:
      {id: bell_cracked, label: The bell cracked, type: log, status: verified,
       text: "The Unsung has 4 more health.", effect: [{op: add, var: boss_bonus, amount: 4}]}

  # Camp upgrades, bought with experience; each effect runs at every later setup (q = the keeper).
  upgrades:
    tempered: {id: tempered, label: Tempered, type: upgrade, cost: 2, status: verified, text: "Your hero has 2 more health.", effect: [{op: add, of_x: "hero_of(q)", attr: health, amount: 2}]}
    clear_voice: {id: clear_voice, label: Clear Voice, type: upgrade, cost: 3, status: verified, text: "Your hero has 1 more resolve.", effect: [{op: add, of_x: "hero_of(q)", attr: resolve, amount: 1}]}
    iron_arm: {id: iron_arm, label: Iron Arm, type: upgrade, cost: 3, status: verified, text: "Your hero has 1 more strike.", effect: [{op: add, of_x: "hero_of(q)", attr: strike, amount: 1}]}
    deep_breath: {id: deep_breath, label: Deep Breath, type: upgrade, cost: 2, status: verified, text: "You start each scenario with 2 echo.", effect: [{op: add, var: "echo[q]", amount: 2}]}
    veteran: {id: veteran, label: A Veteran Joins, type: upgrade, cost: 3, status: verified, text: "Shuffle a Bell-guard Veteran into your deck.", effect: [{op: create, what_x: "content['cards']['bell_guard_veteran']", to: "deck[q]"}]}
    canticle: {id: canticle, label: The Great Canticle, type: upgrade, cost: 3, status: verified, text: "Shuffle a Great Canticle into your deck.", effect: [{op: create, what_x: "content['cards']['great_canticle']", to: "deck[q]"}]}

end:
  - {id: bell_rung, scope: scenario, if: "the last objective is complete (and the Unsung silenced, where she appears)", if_x: "final_stage and progress >= goal and (not content['objectives'][objective].get('boss') or boss_down)", result: win}
  - {id: hush_complete, scope: scenario, if: "the Hush reaches its limit", if_x: "hush >= HUSH_MAX", result: loss}
  - {id: all_silenced, scope: scenario, if: "both heroes have fallen", if_x: "len(standing) == 0", result: loss}
  - {id: nightfall, scope: scenario, if: "the last round is over", if_x: "round_no > ROUND_LIMIT", result: loss}
  - {id: campaign_won, if: "all scenarios played and the Crown of Silence won", if_x: "stage >= len(SCENARIOS) and finale_won", result: win}
  - {id: campaign_lost, if: "all scenarios played and the Crown of Silence lost", if_x: "stage >= len(SCENARIOS) and not finale_won", result: loss}

# Shown while playing, the first time the step comes up (learn as you play)
tutorial:
  - {at: muster_play, text: "Play allies, gear and songs by paying their cost in echo. Tolling the bell pushes the Hush back by one but costs 2 echo. Stop when you are done."}
  - {at: commit, text: "Send ready characters on the venture: their resolve adds up against the dread of the staging area. Committed characters are exhausted and cannot defend this round."}
  - {at: travel_step, text: "Travel to a location in the staging area to make it the active one and start exploring it; staying put changes nothing this round."}
  - {at: opt_engage, text: "Engaging an enemy now means you choose who fights; enemies with a low approach value will come to you anyway once the Hush reaches it."}
  - {at: defence, text: "Defend with a character (exhausting it) or take the blow as damage on your hero. Songs played in this window can help."}
  - {at: strike_back, text: "Attack an engaged enemy with a ready character; damage is strike minus the enemy's ward."}
  - {at: train, text: "Experience buys permanent upgrades for the next scenario. Keeping it is fine too: unspent experience is kept for the next camp."}
