Refine Eibenreith Ink bucket architecture

This commit is contained in:
2026-05-24 11:21:57 +02:00
parent d42540f29d
commit 01c8b1aff6
14 changed files with 1522 additions and 1400 deletions
+171 -119
View File
@@ -8,6 +8,7 @@
// - 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
//
@@ -52,17 +53,56 @@ 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 ===
{
- 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
=== 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 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 tutorial_state = ()
VAR class = ()
VAR title_part = ""
@@ -103,6 +143,23 @@ VAR route_eccentric = 0
// 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 counters are simple totals, not Ingold-style two-value axes. Do not use
// them with relationship matrix helpers.
=== function route(value) ===
~ return value
@@ -137,119 +194,18 @@ VAR route_eccentric = 0
=== 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.
//
// 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
@@ -284,6 +240,17 @@ VAR route_eccentric = 0
// ============================================================================
// 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)
@@ -364,6 +331,18 @@ VAR route_eccentric = 0
// ============================================================================
// 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
@@ -398,6 +377,11 @@ VAR route_eccentric = 0
// ============================================================================
// 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
@@ -418,6 +402,24 @@ VAR route_eccentric = 0
// 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.
// - 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
@@ -493,6 +495,26 @@ 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_move_to(state): readable alias for state_reach(state).
// - state_reached(state), state_before(state), state_between(a, b),
// 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().
@@ -605,6 +627,17 @@ VAR story_state = ()
~ 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) ===
@@ -633,6 +666,24 @@ VAR story_state = ()
// 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
@@ -696,17 +747,18 @@ VAR story_state = ()
~ 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_share_reached(positive, negative, numerator, denominator) ===
~ temp total = positive + negative
~ return total > 0 && positive * denominator >= total * numerator
=== function rel_high(positive, negative) ===
~ return matrix_high(positive, negative)
~ return rel_share_reached(positive, negative, 9, 10)
=== function rel_up(positive, negative) ===
~ return matrix_up(positive, negative)
~ return rel_share_reached(positive, negative, 7, 10)
=== function rel_down(positive, negative) ===
~ return matrix_down(positive, negative)
~ return rel_share_reached(negative, positive, 7, 10)
=== function rel_low(positive, negative) ===
~ return matrix_low(positive, negative)
~ return rel_share_reached(negative, positive, 9, 10)