938 lines
33 KiB
Plaintext
938 lines
33 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 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 = ()
|
|
{
|
|
- 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:
|
|
// Put claim_choice_gate last in the condition list. Earlier conditions should
|
|
// prove that the choice is valid before this side-effecting helper consumes the
|
|
// gate.
|
|
//
|
|
// * {state_reached(freshen_up_done)} {claim_choice_gate(return_auto)} [...] #auto:return(2)
|
|
=== function claim_choice_gate(gate) ===
|
|
{
|
|
- claimed_choice_gates ? gate:
|
|
~ return false
|
|
- else:
|
|
~ claimed_choice_gates += gate
|
|
~ return true
|
|
}
|
|
|
|
|
|
=== 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.
|
|
// - 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.
|
|
// - 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().
|
|
// - 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)
|
|
|
|
=== 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) ===
|
|
{
|
|
- character == viktor:
|
|
~ return viktor_location == current_location
|
|
- else:
|
|
~ return false
|
|
}
|
|
|
|
=== 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
|
|
}
|
|
|
|
=== function move_companions_to(location) ===
|
|
{
|
|
- accompanied_by(viktor):
|
|
~ viktor_location = location
|
|
}
|
|
|
|
=== 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)
|