// ============================================================================ // 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)