Checkpoint Eibenreith ink architecture

This commit is contained in:
2026-05-24 09:09:41 +02:00
parent beac5a2be3
commit d42540f29d
35 changed files with 12015 additions and 54 deletions
+712
View File
@@ -0,0 +1,712 @@
// ============================================================================
// 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
// - 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 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
// ============================================================================
// 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 tut_choice_intro = false
VAR tut_traversal_intro = false
VAR tut_character_intro = false
VAR tut_dialog_intro = false
VAR tut_optional_intro = false
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
=== 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_min(a, b) ===
{
- a <= b:
~ return a
- else:
~ return b
}
=== function route_max(a, b) ===
{
- a >= b:
~ return a
- else:
~ return b
}
=== function route_diff(positive, negative) ===
~ return positive - negative
=== function route_diff_reached(positive, negative, threshold) ===
~ return positive - negative >= threshold
=== function route_diff_before(positive, negative, threshold) ===
~ return positive - negative <= threshold
=== function route_diff_between(positive, negative, min, max) ===
~ temp d = positive - negative
~ return d >= min && d <= max
=== function route_total() ===
~ return route_composure + route_detective + route_lover + route_sapphic + route_careless + route_eccentric
=== function route_share_reached(value, numerator, denominator) ===
~ temp total = route_total()
~ return total > 0 && value * denominator >= total * numerator
=== function route_share_before(value, numerator, denominator) ===
~ temp total = route_total()
~ return total > 0 && value * denominator < total * numerator
=== function route_high(value) ===
~ return route_share_reached(value, 9, 10)
=== function route_up(value) ===
~ return route_share_reached(value, 7, 10)
=== function route_down(value) ===
~ return route_share_before(value, 3, 10)
=== function route_low(value) ===
~ return route_share_before(value, 1, 10)
=== function matrix_total(positive, negative) ===
~ return positive + negative
=== function matrix_share_reached(positive, negative, numerator, denominator) ===
~ temp total = matrix_total(positive, negative)
~ return total > 0 && positive * denominator >= total * numerator
=== function matrix_high(positive, negative) ===
~ return matrix_share_reached(positive, negative, 9, 10)
=== function matrix_up(positive, negative) ===
~ return matrix_share_reached(positive, negative, 7, 10)
=== function matrix_down(positive, negative) ===
~ return matrix_share_reached(negative, positive, 7, 10)
=== function matrix_low(positive, negative) ===
~ return matrix_share_reached(negative, positive, 9, 10)
=== function axis_positive(axis) ===
{
- axis == order_axis:
~ return route_composure
- axis == inquiry_axis:
~ return route_detective
- axis == allure_axis:
~ return route_lover + route_sapphic
- else:
~ return 0
}
=== function axis_negative(axis) ===
{
- axis == order_axis:
~ return route_eccentric
- axis == inquiry_axis:
~ return route_careless
- axis == allure_axis:
~ return route_composure
- else:
~ return 0
}
=== function axis_high(axis) ===
~ return matrix_high(axis_positive(axis), axis_negative(axis))
=== function axis_up(axis) ===
~ return matrix_up(axis_positive(axis), axis_negative(axis))
=== function axis_down(axis) ===
~ return matrix_down(axis_positive(axis), axis_negative(axis))
=== function axis_low(axis) ===
~ return matrix_low(axis_positive(axis), axis_negative(axis))
// ============================================================================
// TIME-SLOT HELPERS
// ============================================================================
// TimeSlot is an ordered LIST declared in Eibenreith.ink.
// Use these helpers for the daily schedule structure.
=== 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.
=== 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.
=== 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.
=== 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.
=== 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 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
// ============================================================================
// 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 ()
}
// 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)
}
// 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
// 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
=== 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(value, total, numerator, denominator) ===
~ return total > 0 && value * denominator >= total * numerator
=== function rel_high(positive, negative) ===
~ return matrix_high(positive, negative)
=== function rel_up(positive, negative) ===
~ return matrix_up(positive, negative)
=== function rel_down(positive, negative) ===
~ return matrix_down(positive, negative)
=== function rel_low(positive, negative) ===
~ return matrix_low(positive, negative)