Files
ai.interactive.fiction/data/ink-src/eibenreith/helpers.ink
T

1031 lines
36 KiB
Plaintext

// ============================================================================
// 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 claimed_choice_gates = ()
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 ===
-> 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
<- 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
// ============================================================================
// 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_intro_start
- episode_to_run == episode_station_midday:
-> enter_episode(episode_station_midday, noon, -> station_midday, -> station_midday_end, -> station_midday_episode_bucket) -> station_midday
- episode_to_run == episode_carriage_ride:
-> enter_episode(episode_carriage_ride, afternoon, -> carriage_ride, -> carriage_ride_end, -> carriage_ride_episode_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) is a
// tunnel used by run_episode. It installs the episode's start/end/bucket
// targets 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
~ 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,
// then update the contact manager.
// - accompanied_by(character): true when character is in the companion list.
// - companion_join(character), companion_leave(character): update companions.
// - 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, 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 or the repeat look action.
=== function loc(value) ===
~ return current_location == value
=== function loc_move_to(value) ===
~ current_location = value
~ move_companions_to(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 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
=== enter_episode(value, slot, start_bucket, end_bucket, episode_bucket) ===
~ episode_start(value, slot)
~ current_episode_start = start_bucket
~ current_episode_end = end_bucket
~ current_episode_bucket = episode_bucket
->->
=== enter_room(location, entry_bucket, look_bucket, exit_bucket, moment_bucket) ===
~ loc_move_to(location)
~ 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)