// ============================================================================
// EIBENREITH 00 HELPERS
// ============================================================================
// Include after global LIST definitions in Eibenreith.ink and before all
// character relationship and chapter files.
//
// Contains:
// - global non-character variables
// - route counters and route helper functions
// - time-slot, meal-plan, episode, location, and traversal helpers
// - active choice-surface knots and bucket dispatch
// - global story-state storage and LIST/state-tree helpers
// - relationship helper functions
//
// Does not contain:
// - global LIST declarations
// - chapter-specific LIST declarations
// - character relationship variables
// - chapter prose or scene knots
// ============================================================================


// ============================================================================
// GLOBAL SCHEDULE / LOCATION / EPISODE VARIABLES
// ============================================================================
// The LIST values used here are declared in Eibenreith.ink before this file is
// included.

VAR current_day = 1
VAR current_slot = mid_morning
VAR active_episode = episode_train_intro
VAR last_slot_outcome = "normal"

VAR meal_plan = meal_unset

VAR current_location = loc_train_compartment
VAR companions = ()
VAR current_room_entry_bucket = -> empty_bucket
VAR current_moment_bucket = -> empty_bucket
VAR current_room_look_bucket = -> empty_bucket
VAR current_room_exit_bucket = -> empty_bucket
VAR current_episode_bucket = -> empty_bucket
VAR current_episode_start = -> empty_bucket
VAR current_episode_end = -> empty_bucket
VAR current_game_bucket = -> game_bucket
VAR current_companion_transition_bucket = -> empty_bucket
VAR claimed_choice_gates = ()
VAR turn_index = 0
VAR entered_rooms = ()
VAR room_look_available = false
VAR room_enter_turn = 0
VAR traversal_previous_location = loc_train_compartment
VAR traversal_next_location = loc_train_compartment
VAR timers_1 = ()
VAR timers_2 = ()
VAR timers_3 = ()
VAR timers_4 = ()
VAR timers_5 = ()
VAR timers_6 = ()
VAR timers_7 = ()
VAR timers_8 = ()
VAR timers_9 = ()
VAR timers_10 = ()
VAR timers_11 = ()
VAR timers_ready = ()
VAR timers_claimed = ()

VAR contact_seen = ()
VAR contact_present = ()
VAR contact_arrived = ()
VAR contact_departed = ()
VAR contact_first_met = ()
VAR contact_rejoined = ()
VAR contact_transition_fresh = false

VAR slot_early_morning_episode = no_episode
VAR slot_mid_morning_episode = episode_train_intro
VAR slot_noon_episode = episode_station_midday
VAR slot_afternoon_episode = episode_carriage_ride
VAR slot_evening_episode = episode_first_dinner
VAR slot_early_night_episode = no_episode
VAR slot_late_night_episode = no_episode


// ============================================================================
// ACTIVE CHOICE SURFACE / BUCKET DISPATCH
// ============================================================================
// Author-facing rule:
// - End a chosen atomic weave with "-> TURN" when play should continue at the
//   current room/episode choice surface.
// - Do not call provide_choices directly from chapter files. It is the internal
//   implementation behind TURN.
// - Bucket provider knots end with "-> DONE". They only offer choices; they do
//   not themselves count as chosen player turns.
//
// Dispatch order:
// - On first room entry: entry, moment, exits, episode, game.
// - On later room entries: moment, look, exits, episode, game.
//
// This keeps room descriptions one-shot through Ink's own visit tracking while
// preserving the design priority Moment > Room > Episode > Game.
// The actual game_bucket remains in buckets.ink because it is content, not
// helper logic.

=== TURN ===
~ turn_index = turn_index + 1
~ timer_tick()
-> provide_choices


=== provide_choices ===
~ claimed_choice_gates = ()
{
- contact_transition_fresh:
    ~ contact_transition_fresh = false
- else:
    ~ contact_clear_transitions()
}
{
- room_seen_on_enter():
    <- current_moment_bucket
    {
    - room_look_available && room_look_unused_since_enter():
        <- current_room_look_bucket
    }
- else:
    <- current_room_entry_bucket
    <- current_moment_bucket
}
<- current_room_exit_bucket
<- current_episode_bucket
<- current_game_bucket
-> DONE


// Claim a transient gate for this single choice-surface build.
//
// This is not story state. It is cleared at the start of provide_choices and is
// used only to arbitrate between multiple choices that could otherwise surface
// together, especially prioritized #auto choices. The first condition in source
// order that claims a gate returns true; later claims of the same gate return
// false until the next choice surface is built.
//
// Use sparingly, and only where the surface must offer at most one item from a
// family. Normal story memory belongs in Ink callbacks, state_* chains, or
// mark/has/lacks facts.
//
// Example:
// Use claim_choice_gate_if when the choice has any availability condition. Pass
// the whole availability expression as the second parameter so invalid choices
// cannot consume the gate while Ink is checking the surface.
//
// * {claim_choice_gate_if(return_auto, reunion(viktor) && state_reached(freshen_up_done))} [...] #auto
=== function claim_choice_gate(gate) ===
    {
    - claimed_choice_gates ? gate:
        ~ return false
    - else:
        ~ claimed_choice_gates += gate
        ~ return true
    }

=== function claim_choice_gate_if(gate, available) ===
    {
    - not available:
        ~ return false
    - else:
        ~ return claim_choice_gate(gate)
    }


=== empty_bucket ===
-> DONE


// ============================================================================
// TURN TIMERS
// ============================================================================
// Generic named countdown timers for authored content.
//
// Timer IDs are LIST values declared in the root file. A timer is started by
// name and duration:
//
// ~ timer_start(train_lunch_order, 3)
//
// The same timer name can only exist once. Starting it again first removes it
// from every countdown, ready, and claimed bucket, then places it at the new
// duration. Durations 1..10 mean "after this many later player choices"; the
// current choice that starts the timer does not consume one of those turns.
//
// Every TURN advances timers centrally. When a countdown expires, the timer
// remains in timers_ready until content claims it. timer_due(timer_id) returns
// true if the timer is ready or already claimed this turn. On the first true
// ready check it moves the timer from ready to claimed. Claimed timers remain
// queryable for the rest of the current turn and are cleared by the next TURN.
//
// Use timer_due_if(timer_id, available) for choice conditions with additional
// story-state requirements. It only claims the timer if available is true.
// Use timer_claim(timer_id) inside chosen content when that content makes the
// timer-controlled event happen before its natural expiry.

=== function timer_tick() ===
    ~ timers_claimed = ()
    ~ timers_ready += timers_1
    ~ timers_1 = timers_2
    ~ timers_2 = timers_3
    ~ timers_3 = timers_4
    ~ timers_4 = timers_5
    ~ timers_5 = timers_6
    ~ timers_6 = timers_7
    ~ timers_7 = timers_8
    ~ timers_8 = timers_9
    ~ timers_9 = timers_10
    ~ timers_10 = timers_11
    ~ timers_11 = ()

=== function timer_remove(timer_id) ===
    ~ timers_1 -= timer_id
    ~ timers_2 -= timer_id
    ~ timers_3 -= timer_id
    ~ timers_4 -= timer_id
    ~ timers_5 -= timer_id
    ~ timers_6 -= timer_id
    ~ timers_7 -= timer_id
    ~ timers_8 -= timer_id
    ~ timers_9 -= timer_id
    ~ timers_10 -= timer_id
    ~ timers_11 -= timer_id
    ~ timers_ready -= timer_id
    ~ timers_claimed -= timer_id

=== function timer_start(timer_id, turns) ===
    ~ timer_remove(timer_id)
    {
    - turns <= 1:
        ~ timers_2 += timer_id
    - turns == 2:
        ~ timers_3 += timer_id
    - turns == 3:
        ~ timers_4 += timer_id
    - turns == 4:
        ~ timers_5 += timer_id
    - turns == 5:
        ~ timers_6 += timer_id
    - turns == 6:
        ~ timers_7 += timer_id
    - turns == 7:
        ~ timers_8 += timer_id
    - turns == 8:
        ~ timers_9 += timer_id
    - turns == 9:
        ~ timers_10 += timer_id
    - else:
        ~ timers_11 += timer_id
    }

=== function timer_due(timer_id) ===
    {
    - timers_claimed ? timer_id:
        ~ return true
    - timers_ready ? timer_id:
        ~ timers_ready -= timer_id
        ~ timers_claimed += timer_id
        ~ return true
    - else:
        ~ return false
    }

=== function timer_due_if(timer_id, available) ===
    {
    - not available:
        ~ return false
    - else:
        ~ return timer_due(timer_id)
    }

=== function timer_claim(timer_id) ===
    ~ timer_remove(timer_id)
    ~ timers_claimed += timer_id

=== function timer_pending(timer_id) ===
    ~ return timers_1 ? timer_id || timers_2 ? timer_id || timers_3 ? timer_id || timers_4 ? timer_id || timers_5 ? timer_id || timers_6 ? timer_id || timers_7 ? timer_id || timers_8 ? timer_id || timers_9 ? timer_id || timers_10 ? timer_id || timers_11 ? timer_id || timers_ready ? timer_id || timers_claimed ? timer_id


// ============================================================================
// INTRO / CHARACTER-GENERATION VARIABLES
// ============================================================================
// These values are defined during chapter 01, but they are global character
// facts and may be referenced by later chapters.

VAR tutorial_state = ()

VAR class = ()
VAR title_part = ""
VAR given_names = ""
VAR common_name = ""
VAR surname = ""
VAR birthplace = ""
VAR birthdate = ""
VAR zodiac = ""

VAR relig = ()
VAR belief = ()
VAR body = ()
VAR outfit = ()
VAR hair = ()
VAR style = ()
VAR face = ()


// ============================================================================
// VALERIE ROUTE VALUES
// ============================================================================
// These are counters, not mutually exclusive classes.
// They describe the strategies the player repeatedly chooses for Valerie.

VAR route_composure = 0
VAR route_detective = 0
VAR route_lover = 0
VAR route_sapphic = 0
VAR route_careless = 0
VAR route_eccentric = 0


// ============================================================================
// ROUTE HELPERS
// ============================================================================
// Ink does not support overloaded functions or optional parameters.
// Therefore:
// - route_inc(route) increases by 1
// - route_inc_by(route, amount) increases by amount
//
// Author-facing helpers:
// - route(value): returns the current counter value for interpolation/debugging.
// - route_inc(ref route): add 1 to a route counter.
// - route_inc_by(ref route, amount): add an explicit amount.
// - route_dec(ref route): subtract 1.
// - route_dec_by(ref route, amount): subtract an explicit amount.
// - route_move_to(ref route, amount): set an exact route counter value.
// - route_clear(ref route): reset a route counter to 0.
// - route_is(route, amount): true when the counter equals amount.
// - route_before(route, amount): true when the counter is <= amount.
// - route_reached(route, amount): true when the counter is >= amount.
// - route_between(route, min, max): true when the counter is within the
//   inclusive range min..max.
// - route_value(route_id): returns a route counter by RouteId LIST item.
// - route_total(): total number of route-marking choices taken.
// - route_beats(route_id, other_route_id, margin): true when one route is ahead
//   of another by at least margin.
// - route_repeated(route_id, amount): true when a route has been chosen at
//   least amount times. Use for "player keeps choosing this mode" heuristics.
// - route_is_highest(route_id): true when a route is tied for or holds the
//   highest current route total and has been chosen at least once.
// - route_is_clear(route_id, margin): true when a route leads every other route
//   by at least margin. Use this for strong heuristic tone changes.
// - route_share_reached(route_id, numerator, denominator): true when a route is
//   at least numerator/denominator of all route-marking choices.
//
// Route counters are simple totals, not Ingold-style two-value axes. Do not use
// them with relationship matrix helpers.

=== function route(value) ===
    ~ return value

=== function route_inc(ref value) ===
    ~ value += 1

=== function route_inc_by(ref value, amount) ===
    ~ value += amount

=== function route_dec(ref value) ===
    ~ value -= 1

=== function route_dec_by(ref value, amount) ===
    ~ value -= amount

=== function route_move_to(ref value, amount) ===
    ~ value = amount

=== function route_clear(ref value) ===
    ~ value = 0

=== function route_is(value, amount) ===
    ~ return value == amount

=== function route_before(value, amount) ===
    ~ return value <= amount

=== function route_reached(value, amount) ===
    ~ return value >= amount

=== function route_between(value, min, max) ===
    ~ return value >= min && value <= max

=== function route_value(route_id) ===
    {
    - route_id ? composure:
        ~ return route_composure
    - route_id ? detective:
        ~ return route_detective
    - route_id ? lover:
        ~ return route_lover
    - route_id ? sapphic:
        ~ return route_sapphic
    - route_id ? careless:
        ~ return route_careless
    - route_id ? eccentric:
        ~ return route_eccentric
    - else:
        ~ return 0
    }

=== function route_total() ===
    ~ return route_composure + route_detective + route_lover + route_sapphic + route_careless + route_eccentric

=== function route_best_value() ===
    {
    - route_composure >= route_detective && route_composure >= route_lover && route_composure >= route_sapphic && route_composure >= route_careless && route_composure >= route_eccentric:
        ~ return route_composure
    - route_detective >= route_composure && route_detective >= route_lover && route_detective >= route_sapphic && route_detective >= route_careless && route_detective >= route_eccentric:
        ~ return route_detective
    - route_lover >= route_composure && route_lover >= route_detective && route_lover >= route_sapphic && route_lover >= route_careless && route_lover >= route_eccentric:
        ~ return route_lover
    - route_sapphic >= route_composure && route_sapphic >= route_detective && route_sapphic >= route_lover && route_sapphic >= route_careless && route_sapphic >= route_eccentric:
        ~ return route_sapphic
    - route_careless >= route_composure && route_careless >= route_detective && route_careless >= route_lover && route_careless >= route_sapphic && route_careless >= route_eccentric:
        ~ return route_careless
    - route_eccentric >= route_composure && route_eccentric >= route_detective && route_eccentric >= route_lover && route_eccentric >= route_sapphic && route_eccentric >= route_careless:
        ~ return route_eccentric
    - else:
        ~ return 0
    }

=== function route_beats(route_id, other_route_id, margin) ===
    ~ return route_value(route_id) - route_value(other_route_id) >= margin

=== function route_repeated(route_id, amount) ===
    ~ return route_value(route_id) >= amount

=== function route_is_highest(route_id) ===
    ~ return route_value(route_id) > 0 && route_value(route_id) >= route_best_value()

=== function route_is_clear(route_id, margin) ===
    {
    - route_id ? composure:
        ~ return route_composure > 0 && route_composure - route_detective >= margin && route_composure - route_lover >= margin && route_composure - route_sapphic >= margin && route_composure - route_careless >= margin && route_composure - route_eccentric >= margin
    - route_id ? detective:
        ~ return route_detective > 0 && route_detective - route_composure >= margin && route_detective - route_lover >= margin && route_detective - route_sapphic >= margin && route_detective - route_careless >= margin && route_detective - route_eccentric >= margin
    - route_id ? lover:
        ~ return route_lover > 0 && route_lover - route_composure >= margin && route_lover - route_detective >= margin && route_lover - route_sapphic >= margin && route_lover - route_careless >= margin && route_lover - route_eccentric >= margin
    - route_id ? sapphic:
        ~ return route_sapphic > 0 && route_sapphic - route_composure >= margin && route_sapphic - route_detective >= margin && route_sapphic - route_lover >= margin && route_sapphic - route_careless >= margin && route_sapphic - route_eccentric >= margin
    - route_id ? careless:
        ~ return route_careless > 0 && route_careless - route_composure >= margin && route_careless - route_detective >= margin && route_careless - route_lover >= margin && route_careless - route_sapphic >= margin && route_careless - route_eccentric >= margin
    - route_id ? eccentric:
        ~ return route_eccentric > 0 && route_eccentric - route_composure >= margin && route_eccentric - route_detective >= margin && route_eccentric - route_lover >= margin && route_eccentric - route_sapphic >= margin && route_eccentric - route_careless >= margin
    - else:
        ~ return false
    }

=== function route_share_reached(route_id, numerator, denominator) ===
    ~ temp total = route_total()
    ~ return total > 0 && route_value(route_id) * denominator >= total * numerator

// ============================================================================
// TIME-SLOT HELPERS
// ============================================================================
// TimeSlot is an ordered LIST declared in Eibenreith.ink.
// Use these helpers for the daily schedule structure.
//
// Author-facing helpers:
// - day(value), day_move_to(value), day_inc()
// - time(slot), time_is(slot), time_move_to(slot)
// - time_before(slot), time_reached(slot), time_between(first, last)
//
// time_before/reached/between rely on TimeSlot LIST order in main.ink.

=== function day(value) ===
    ~ return current_day == value

=== function day_move_to(value) ===
    ~ current_day = value

=== function day_inc() ===
    ~ current_day += 1

=== function time(slot) ===
    ~ return current_slot == slot

=== function time_move_to(slot) ===
    ~ current_slot = slot

=== function time_is(slot) ===
    ~ return current_slot == slot

=== function time_before(slot) ===
    ~ return current_slot < slot

=== function time_reached(slot) ===
    ~ return current_slot >= slot

=== function time_between(first, last) ===
    ~ return current_slot >= first && current_slot <= last


// ============================================================================
// TIMETABLE / EPISODE DISPATCH
// ============================================================================
// Content files define episode entry knots and buckets. Timetable control lives
// here so authored episode files do not choose themselves.
//
// Author-facing helpers:
// - slot_episode(slot, episode): query what episode is assigned to a timetable
//   slot.
// - slot_schedule(slot, episode): assign an episode to a timetable slot.
// - advance_to_slot(slot): move time forward and run the slot's episode.
//
// Internal knots:
// - start_game, run_slot(slot), and run_episode(episode) are dispatch knots used
//   by the root entry point and timetable. Chapter files should normally not
//   bypass them.

=== start_game ===
-> run_slot(mid_morning)

=== run_slot(slot) ===
{
- slot == mid_morning:
    -> run_episode(slot_mid_morning_episode)
- slot == noon:
    -> run_episode(slot_noon_episode)
- slot == afternoon:
    -> run_episode(slot_afternoon_episode)
- slot == evening:
    -> run_episode(slot_evening_episode)
- else:
    -> END
}

=== function slot_episode(slot, episode_to_run) ===
    {
    - slot == early_morning:
        ~ return slot_early_morning_episode == episode_to_run
    - slot == mid_morning:
        ~ return slot_mid_morning_episode == episode_to_run
    - slot == noon:
        ~ return slot_noon_episode == episode_to_run
    - slot == afternoon:
        ~ return slot_afternoon_episode == episode_to_run
    - slot == evening:
        ~ return slot_evening_episode == episode_to_run
    - slot == early_night:
        ~ return slot_early_night_episode == episode_to_run
    - slot == late_night:
        ~ return slot_late_night_episode == episode_to_run
    - else:
        ~ return false
    }

=== function slot_schedule(slot, episode_to_run) ===
    {
    - slot == early_morning:
        ~ slot_early_morning_episode = episode_to_run
    - slot == mid_morning:
        ~ slot_mid_morning_episode = episode_to_run
    - slot == noon:
        ~ slot_noon_episode = episode_to_run
    - slot == afternoon:
        ~ slot_afternoon_episode = episode_to_run
    - slot == evening:
        ~ slot_evening_episode = episode_to_run
    - slot == early_night:
        ~ slot_early_night_episode = episode_to_run
    - slot == late_night:
        ~ slot_late_night_episode = episode_to_run
    }

=== run_episode(episode_to_run) ===
{
- episode_to_run == episode_train_intro:
    -> enter_episode(episode_train_intro, mid_morning, -> train_intro_start, -> train_intro_end, -> train_intro_episode_bucket, -> train_companion_transition_bucket) -> train_intro_start
- episode_to_run == episode_station_midday:
    -> enter_episode(episode_station_midday, noon, -> station_midday, -> station_midday_end, -> station_midday_episode_bucket, -> empty_bucket) -> station_midday
- episode_to_run == episode_carriage_ride:
    -> enter_episode(episode_carriage_ride, afternoon, -> carriage_ride, -> carriage_ride_end, -> carriage_ride_episode_bucket, -> empty_bucket) -> carriage_ride
- episode_to_run == episode_first_dinner:
    -> pre_dinner_transition
- else:
    -> END
}

=== advance_to_slot(slot) ===
~ current_slot = slot
-> run_slot(slot)


// ============================================================================
// EPISODE HELPERS
// ============================================================================
// EpisodeId is a LIST declared in Eibenreith.ink.
// active_episode is the coarse structural episode currently being played.
//
// Author-facing helpers:
// - episode(value), episode_active(value): true when the given episode is active.
// - episode_move_to(value): manually change active_episode.
// - episode_end(outcome): close the active episode and clear episode buckets.
// - outcome_is(value), outcome_move_to(value): query/set the last slot outcome.
//
// Structural helper:
// - enter_episode(value, slot, start_bucket, end_bucket, episode_bucket,
//   companion_transition_bucket) is a tunnel used by run_episode. It installs
//   the episode's start/end/content buckets, the optional companion traversal
//   prose bucket, and resets the slot outcome. It should be called from
//   timetable dispatch, not from random room content.

=== function episode(value) ===
    ~ return active_episode == value

=== function episode_start(value, slot) ===
    ~ active_episode = value
    ~ current_slot = slot
    ~ last_slot_outcome = "normal"

=== function episode_move_to(value) ===
    ~ active_episode = value

=== function episode_end(outcome) ===
    ~ active_episode = no_episode
    ~ current_episode_bucket = -> empty_bucket
    ~ current_episode_start = -> empty_bucket
    ~ current_episode_end = -> empty_bucket
    ~ current_companion_transition_bucket = -> empty_bucket
    ~ last_slot_outcome = outcome

=== function episode_active(value) ===
    ~ return active_episode == value

=== function outcome_is(value) ===
    ~ return last_slot_outcome == value

=== function outcome_move_to(value) ===
    ~ last_slot_outcome = value


// ============================================================================
// MEAL-PLAN HELPERS
// ============================================================================
// MealPlan is a LIST declared in Eibenreith.ink.
// The arrival day uses this to remember how lunch is handled.
//
// Author-facing helpers:
// - meal(value), meal_is(value): true when the selected plan equals value.
// - meal_choose(value): set the plan.
// - meal_clear(): reset to meal_unset.

=== function meal(value) ===
    ~ return meal_plan == value

=== function meal_choose(value) ===
    ~ meal_plan = value

=== function meal_is(value) ===
    ~ return meal_plan == value

=== function meal_clear() ===
    ~ meal_plan = meal_unset


// ============================================================================
// LOCATION HELPERS
// ============================================================================
// Location is a LIST declared in Eibenreith.ink.
// current_location is intentionally coarse and exists so episode/game buckets
// can react to where Valerie currently is.
//
// Author-facing helpers:
// - loc(value), loc_is(value): true when Valerie is at location.
// - loc_move_to(value): move Valerie and all current companions to location,
//   record traversal origin/destination, then update the contact manager.
// - accompanied_by(character): true when character is in the companion list.
// - companion_join(character), companion_leave(character): update companions.
// - traversal_from(location), traversal_to(location), traversal_between(a, b):
//   true for the movement that just entered the current room. Use only inside
//   companion transition buckets.
// - present(character): true when a character's location equals current_location.
// - first_meeting(character): true on the choice surface where Valerie meets
//   that tracked character for the first time.
// - reunion(character): true on the choice surface where Valerie sees a known
//   tracked character again after they were apart.
// - parting(character): true on the choice surface after a tracked character
//   has just stopped being present.
// - alone(): true when no tracked NPC is currently present with Valerie.
// - alone_with(character): true when exactly the given tracked NPC is present.
// - character_move_to(character, location): move a known NPC independently.
//
// Structural helper:
// - enter_room(location, entry_bucket, look_bucket, exit_bucket, moment_bucket)
//   moves Valerie, updates companions, prints the active companion transition
//   bucket, and installs the active room buckets.
//   Room knots should normally do nothing except call enter_room and then TURN.
//
// Internal helper:
// - move_companions_to(location) is used by loc_move_to().
// - contact_update() refreshes first-meeting, reunion, and parting transitions.
// - contact_sync() establishes current contact without transitions, useful
//   after episode setup that places multiple characters at once.
// - contact_clear_transitions() is called internally by provide_choices. Content
//   must not consume or clear contact transitions manually.
// - characters_at(location) must be extended when a new tracked NPC is added.
// - room_seen_on_enter() is used by provide_choices() to decide whether to show
//   the one-shot entry text.
// - room_look_unused_since_enter() uses Ink visit tracking to hide the look
//   bucket after it was used during the current room visit. Chapter content must
//   not create custom look flags or call room helpers manually.

=== function loc(value) ===
    ~ return current_location == value

=== function loc_move_to(value) ===
    ~ temp previous_location = current_location
    ~ traversal_previous_location = previous_location
    ~ traversal_next_location = value
    ~ current_location = value
    ~ move_companions_to(value)
    ~ room_update_on_move(previous_location, value)
    ~ contact_update()

=== function loc_is(value) ===
    ~ return current_location == value

=== function accompanied_by(character) ===
    ~ return companions ? character

=== function companion_join(character) ===
    ~ companions += character

=== function companion_leave(character) ===
    ~ companions -= character

=== function traversal_from(location) ===
    ~ return traversal_previous_location == location

=== function traversal_to(location) ===
    ~ return traversal_next_location == location

=== function traversal_between(origin, destination) ===
    ~ return traversal_from(origin) && traversal_to(destination)

=== function present(character) ===
    ~ return characters_at(current_location) ? character

=== function first_meeting(character) ===
    ~ return contact_first_met ? character

=== function reunion(character) ===
    ~ return contact_rejoined ? character

=== function parting(character) ===
    ~ return contact_departed ? character

=== function alone() ===
    ~ return not present(viktor)

=== function alone_with(character) ===
    {
    - character == viktor:
        ~ return present(viktor)
    - else:
        ~ return false
    }

=== function character_move_to(character, location) ===
    {
    - character == viktor:
        ~ viktor_location = location
    }
    ~ contact_update()

=== function move_companions_to(location) ===
    {
    - accompanied_by(viktor):
        ~ viktor_location = location
    }

=== function characters_at(location) ===
    ~ temp result = ()
    {
    - viktor_location == location:
        ~ result += viktor
    }
    ~ return result

=== function contact_sync() ===
    ~ contact_present = characters_at(current_location)
    ~ contact_seen += contact_present
    ~ contact_arrived = ()
    ~ contact_departed = ()
    ~ contact_first_met = ()
    ~ contact_rejoined = ()
    ~ contact_transition_fresh = false

=== function contact_update() ===
    ~ temp previous = contact_present
    ~ contact_present = characters_at(current_location)
    ~ contact_arrived = ()
    ~ contact_departed = ()
    ~ contact_first_met = ()
    ~ contact_rejoined = ()
    {
    - (contact_present ? viktor) && not (previous ? viktor):
        ~ contact_arrived += viktor
        {
        - contact_seen ? viktor:
            ~ contact_rejoined += viktor
        - else:
            ~ contact_first_met += viktor
        }
    }
    {
    - (previous ? viktor) && not (contact_present ? viktor):
        ~ contact_departed += viktor
    }
    ~ contact_seen += contact_present
    ~ contact_transition_fresh = false
    {
    - contact_arrived || contact_departed || contact_first_met || contact_rejoined:
        ~ contact_transition_fresh = true
    }

=== function contact_clear_transitions() ===
    ~ contact_arrived = ()
    ~ contact_departed = ()
    ~ contact_first_met = ()
    ~ contact_rejoined = ()
    ~ contact_transition_fresh = false

=== function room_update_on_move(previous_location, next_location) ===
    {
    - previous_location != next_location:
        ~ room_enter_turn = turn_index
        {
        - entered_rooms ? next_location:
            ~ room_look_available = true
        - else:
            ~ room_look_available = false
        }
        ~ entered_rooms += next_location
    - not (entered_rooms ? next_location):
        ~ room_enter_turn = turn_index
        ~ room_look_available = false
        ~ entered_rooms += next_location
    }

=== function room_look_unused_since_enter() ===
    ~ temp turns_since_look = TURNS_SINCE(current_room_look_bucket)
    {
    - turns_since_look < 0:
        ~ return true
    - else:
        ~ return turn_index - turns_since_look < room_enter_turn
    }

=== enter_episode(value, slot, start_bucket, end_bucket, episode_bucket, companion_transition_bucket) ===
    ~ episode_start(value, slot)
    ~ current_episode_start = start_bucket
    ~ current_episode_end = end_bucket
    ~ current_episode_bucket = episode_bucket
    ~ current_companion_transition_bucket = companion_transition_bucket
->->

=== enter_room(location, entry_bucket, look_bucket, exit_bucket, moment_bucket) ===
    ~ loc_move_to(location)
    -> current_companion_transition_bucket ->
    ~ current_room_entry_bucket = entry_bucket
    ~ current_moment_bucket = moment_bucket
    ~ current_room_look_bucket = look_bucket
    ~ current_room_exit_bucket = exit_bucket
->->

=== function room_seen_on_enter() ===
    ~ return TURNS_SINCE(current_room_entry_bucket) >= 0

// ============================================================================
// GLOBAL STORY-STATE STORAGE
// ============================================================================
// Chapter-specific state machines are declared as LISTs in the main file,
// directly above the chapter index they belong to.
//
// story_state stores all reached states from all LIST-based state machines.
// State functions below implement high-watermark logic:
// reaching a later state automatically reaches all earlier states in the same
// LIST chain.

VAR story_state = ()


// ============================================================================
// STATE HELPERS
// ============================================================================
// There are two kinds of story state:
// - Ordered encounter/progress states use state_reach/state_reached. Reaching a
//   later value automatically counts all earlier values in the same LIST.
// - Independent checklist facts use mark/has/lacks. They do not imply their
//   neighbours in the LIST.
//
// Author-facing helpers:
// - state_reach(state_or_states): advance one or more ordered LIST states.
// - state_reach_if_started(state_or_states): advance only chains that already
//   have any reached state; useful when one action can complete an already
//   started task but must not start it retroactively.
// - state_move_to(state): readable alias for state_reach(state).
// - state_reached(state), state_before(state), state_between(a, b),
//   state_started(state), state_unstarted(state), state_is(state),
//   state_current(state): query ordered progress.
// - mark(fact_or_facts), has(fact_or_facts), lacks(fact_or_facts): manage exact
//   checklist facts.
// - tutorial(tutorial_fact): returns true once and marks the tutorial as shown.
// - state_clear(state), state_clear_all(): reset helpers for rare debug/dream
//   logic, not normal authoring.
//
// Internal helper:
// - state_pop(ref list) exists only so state_reach can process multi-value
//   lists. Do not use it in chapter prose.

// Remove and return the lowest item from a list.
// Internal helper for state_reach().
=== function state_pop(ref list) ===
    ~ temp x = LIST_MIN(list)
    ~ list -= x
    ~ return x


// Return true if the given forward state has been reached.
// Use only with one-way encounter/progress LISTs advanced through state_reach().
// For independent checklist facts, use has() / lacks() / mark().
// Example:
// {state_reached(CourtMission.hidden_instruction_revealed): ...}
=== function state_reached(state) ===
    ~ return LIST_COUNT(story_state ^ state) > 0


// Return true if the given forward state has not yet been reached.
// Use only with one-way encounter/progress LISTs advanced through state_reach().
// Example:
// {state_before(CourtMission.hidden_instruction_revealed): ...}
=== function state_before(state) ===
    ~ return not state_reached(state)


// Return true if lower_state has been reached but upper_state has not.
// Lower bound inclusive, upper bound exclusive.
// Example:
// {state_between(CourtMission.official_cover_understood, CourtMission.hidden_instruction_revealed): ...}
=== function state_between(lower_state, upper_state) ===
    ~ return state_reached(lower_state) && not state_reached(upper_state)


// Return true if state is the highest reached state in its LIST chain.
// This is the practical equivalent of current-state testing for a high-watermark chain.
=== function state_is(state) ===
    ~ temp chain = LIST_ALL(state)
    ~ temp reached_in_chain = story_state ^ chain
    {
    - reached_in_chain:
        ~ return LIST_MAX(reached_in_chain) == state
    - else:
        ~ return false
    }


// Return the highest reached state in the same LIST chain as state.
// Useful mainly for debugging or diagnostic text.
=== function state_current(state) ===
    ~ temp chain = LIST_ALL(state)
    ~ temp reached_in_chain = story_state ^ chain
    {
    - reached_in_chain:
        ~ return LIST_MAX(reached_in_chain)
    - else:
        ~ return ()
    }


// Return true if any state in the same LIST chain has been reached.
// This is the generic "not null" check for a progress tracker.
//
// Use when content needs to know whether a linear process has begun at all,
// without caring which exact step is current.
//
// Example:
// {state_started(freshen_up_done): ...}
=== function state_started(state) ===
    ~ return LIST_COUNT(story_state ^ LIST_ALL(state)) > 0


// Return true if no state in the same LIST chain has been reached.
// This is the generic null-state check for a progress tracker.
//
// Example:
// {state_unstarted(freshen_up_started): ...}
=== function state_unstarted(state) ===
    ~ return not state_started(state)


// Mark a state, or a list of states, as reached.
// High-watermark behavior:
// reaching a state automatically reaches all earlier states in the same LIST chain.
//
// Examples:
// ~ state_reach(CourtMission.hidden_instruction_revealed)
// ~ state_reach((CourtMission.official_cover_understood, ViktorRelationFrame.handler_role_understood))
=== function state_reach(states_to_set) ===
    ~ temp x = state_pop(states_to_set)
    {
    - not x:
        ~ return false

    - not state_reached(x):
        ~ temp chain = LIST_ALL(x)
        ~ temp states_gained = LIST_RANGE(chain, LIST_MIN(chain), x)
        ~ story_state += states_gained
        ~ state_reach(states_to_set)
        ~ return true

    - else:
        ~ return false || state_reach(states_to_set)
    }


// Mark a state, or list of states, as reached only if its own LIST chain has
// already begun.
//
// Use this for actions that can satisfy an already stated intention or progress
// an already active line, but must not create that line retroactively.
//
// Example:
// ~ state_reach_if_started(freshen_up_done)
// ~ state_reach_if_started((freshen_up_done, unwell_managed))
=== function state_reach_if_started(states_to_set) ===
    ~ temp x = state_pop(states_to_set)
    {
    - not x:
        ~ return false

    - state_started(x):
        ~ return state_reach(x) || state_reach_if_started(states_to_set)

    - else:
        ~ return false || state_reach_if_started(states_to_set)
    }


// Return true if every given independent fact has been marked.
// Facts are exact checklist items, not forward-moving encounter states.
// Example:
// {has(class_def)}
// {has((class_def, name_def))}
=== function has(facts) ===
    ~ return story_state ? facts


// Return true if at least one given independent fact is still missing.
// Example:
// {lacks(relig_def)}
// {lacks((body_def, face_def))}
=== function lacks(facts) ===
    ~ return not has(facts)


// Mark an independent fact, or a list of independent facts.
// Unlike state_reach(), this does not imply earlier items in the same LIST.
// Example:
// ~ mark(class_def)
// ~ mark((class_def, name_def))
=== function mark(facts_to_set) ===
    ~ story_state += facts_to_set
    ~ return true


// Return true once for a tutorial prompt, and mark it as seen immediately.
=== function tutorial(tutorial_to_show) ===
    {
    - tutorial_state ? tutorial_to_show:
        ~ return false
    - else:
        ~ tutorial_state += tutorial_to_show
        ~ return true
    }


// Alias for readability when scene code wants to say move_to.
// Same high-watermark behavior as state_reach().
=== function state_move_to(state) ===
    ~ state_reach(state)


// Remove all reached states in the same LIST chain as state.
// Use rarely: debug, dream logic, simulation reset, or deliberate memory loss.
=== function state_clear(state) ===
    ~ story_state -= LIST_ALL(state)


// Clear all reached states from all state machines.
// Use only for debug or full reinitialization.
=== function state_clear_all() ===
    ~ story_state = ()


// ============================================================================
// RELATIONSHIP HELPERS
// ============================================================================
// Relationship variables are defined in eibenreith_00_character_relationships.ink.
// These helpers operate on those variables by reference.
//
// Ink does not support overloaded functions or optional parameters.
// Therefore:
// - rel_inc(rel) increases by 1
// - rel_inc_by(rel, amount) increases by amount
//
// Author-facing helpers:
// - rel(value): returns the current counter value for interpolation/debugging.
// - rel_inc/ref, rel_inc_by/ref, rel_dec/ref, rel_dec_by/ref: change a counter.
// - rel_move_to(ref value, amount), rel_clear(ref value): set/reset a counter.
// - rel_is(value, amount), rel_before(value, amount),
//   rel_reached(value, amount), rel_between(value, min, max): numeric queries.
// - rel_diff(positive, negative) and rel_diff_* compare a two-value axis by
//   subtraction.
// - rel_share_reached(positive, negative, numerator, denominator) and
//   rel_high/up/down/low implement Ingold-style percentage queries for a pair.
//
// Use relationship helpers with the five standard relationship pairs, e.g.:
// {rel_up(viktor_open, viktor_closed)}
// {rel_high(amalia_reliable, amalia_unreliable)}
//
// Do not create per-character custom dimensions. If a concept does not fit the
// standard pairs, express it in prose or in a semantic encounter LIST.

=== function rel(value) ===
    ~ return value

=== function rel_inc(ref value) ===
    ~ value += 1

=== function rel_inc_by(ref value, amount) ===
    ~ value += amount

=== function rel_dec(ref value) ===
    ~ value -= 1

=== function rel_dec_by(ref value, amount) ===
    ~ value -= amount

=== function rel_move_to(ref value, amount) ===
    ~ value = amount

=== function rel_clear(ref value) ===
    ~ value = 0

=== function rel_is(value, amount) ===
    ~ return value == amount

=== function rel_before(value, amount) ===
    ~ return value <= amount

=== function rel_reached(value, amount) ===
    ~ return value >= amount

=== function rel_between(value, min, max) ===
    ~ return value >= min && value <= max

=== function rel_min(a, b) ===
    {
    - a <= b:
        ~ return a
    - else:
        ~ return b
    }

=== function rel_max(a, b) ===
    {
    - a >= b:
        ~ return a
    - else:
        ~ return b
    }

=== function rel_diff(positive, negative) ===
    ~ return positive - negative

=== function rel_diff_reached(positive, negative, threshold) ===
    ~ return positive - negative >= threshold

=== function rel_diff_before(positive, negative, threshold) ===
    ~ return positive - negative <= threshold

=== function rel_diff_between(positive, negative, min, max) ===
    ~ temp d = positive - negative
    ~ return d >= min && d <= max

=== function rel_share_reached(positive, negative, numerator, denominator) ===
    ~ temp total = positive + negative
    ~ return total > 0 && positive * denominator >= total * numerator

=== function rel_high(positive, negative) ===
    ~ return rel_share_reached(positive, negative, 9, 10)

=== function rel_up(positive, negative) ===
    ~ return rel_share_reached(positive, negative, 7, 10)

=== function rel_down(positive, negative) ===
    ~ return rel_share_reached(negative, positive, 7, 10)

=== function rel_low(positive, negative) ===
    ~ return rel_share_reached(negative, positive, 9, 10)
