Repository navigation
Events
Audience: Developers writing Mind the School scenes — the events players reach by exploring buildings and through story triggers. This guide covers every event class, how events are defined and registered, and — in depth — how the scene label is written: reading selector values, showing images and videos, using characters, and the event decision-menu system used for branching and replay.
Scope: The event system (
event.rpy), its scene helpers (images.rpy,menu.rpy,gallery.rpy,character.rpy) and how content files wire it up. Events lean on: Conditions (gates), Selectors (dynamic values), Effects / Modifiers (consequences), Options (flags), Building Situations (event pools), and School Levels (what the campus sounds and looks like at the current school level).
An event is two things that share a name: a definition (Event(...), holding
metadata) registered into a pool, and a scene label (label <name>(**kwargs):) —
the Ren'Py scene that plays.
init 1 python:
set_current_mod('base')
my_event = Event(3, "cafeteria_snack_chat", # priority 3, label name
TimeCondition(daytime="d"), # gate: only at daytime "d"
RandomListSelector("topic", "coffee", "tea"), # dynamic value → <topic>
Pattern("main", "images/events/cafeteria/snack_chat/<topic> <step>.webp"),
thumbnail="images/events/cafeteria/snack_chat/coffee 1.webp")
cafeteria_events["order_food"].add_event(my_event) # into the pool
label cafeteria_snack_chat(**kwargs):
$ begin_event(**kwargs)
$ topic = get_value("topic", **kwargs) # read the rolled selector
$ image = convert_pattern("main", **kwargs) # build the stepped image
$ image.show(0)
"She sips her [topic]."
$ image.show(1)
call change_stats_with_modifier(happiness=SMALL) from _e1
$ end_event("new_daytime", **kwargs)Everything below expands each piece.
- The event lifecycle
- The event classes
- Storages & pools
- Defining an event
- Priorities & availability
- Registering & wiring into locations
- The scene label: begin_event / end_event
- Reading selector values in the label
- Images: patterns, steps & Image_Series
- Videos
- Characters & dialogue
- The decision-menu system
- Changing stats & progress
- Composite events & fragments
- Select events
- Gallery, seen-tracking & replay
- Modding events
- Conventions
- Troubleshooting
- Reference tables
-
Definition — an
Event(...)object is built in aninit pythonblock and holds priority, gates (conditions), dynamic values (selectors), image patterns and a thumbnail. It registers itself into the globalevent_registerand is added to a pool. -
Selection — the player enters a location and picks an action. The location's
EventStoragegathers available events (all conditions pass) and, by priority, selects what runs (see §5). If nothing is available, the pool's fallback runs. -
Call — the selected event's
call()merges its rolled selector values intokwargs["values"], attaches its patterns askwargs["image_patterns"], setsevent_name/event_obj, andrenpy.calls the scene label. -
Scene — the
labelruns:begin_event, then images/dialogue/menus/stat changes, thenend_event, which returns to the map (or advances the day) and ticks the surrounding systems (situations, quests).
The definition and the label are joined only by the event string = label name.
check_event() validates that the label exists (renpy.has_label) and the priority
is 1–3; failures are logged (category event) and the event is marked invalid.
All four subclass Event; the differences are how they select/run their content.
Event(select_type, event, *conditions_selectors_options_patterns,
thumbnail="", register_self=True, override_location=None)-
select_type— priority 1/2/3 (§5). -
event— the scene label name (string). -
*args— a mixed list sorted by type:Condition(gates),Selector(values),Option(flags),Pattern(images) — in any order (§4). -
thumbnail— journal/menu image (falls back tojournal/empty_image_wide.webp). -
register_self— register intoevent_register(leaveTrue; seen-tracking and replay need it). -
override_location— force the event's location tag.
EventComposite(priority, event, fragments: List[FragmentStorage], *conditions, thumbnail="")A wrapper scene that plays a sequence of fragments pulled from the given
FragmentStorages. Its own label runs an intro and then hands off to
composite_event_runner, which selects fragments (select_fragments) and plays them
in turn (call_fragment). Used for events with a repeating, shuffled middle (e.g.
Truth or Dare: a random number of truth/dare fragments, then an end fragment). See
§14.
EventFragment(select_type, event, *conditions, thumbnail="")A scene that lives in a FragmentStorage (location "fragment") and is pulled into a
composite. Authored like a normal event label; it does not register into a
location pool.
EventSelect(priority, event, text, event_list: Dict[str, EventStorage], *conditions,
thumbnail="", override_menu_exit="map_entry", fallback=None, person=None)Presents a menu (text, spoken by person) that routes to one of several
EventStorages in event_list; its label is select_event_runner, which calls the
location action menu (call_event_menu) and, on exit, jumps to override_menu_exit.
Available only if at least one storage in event_list has an available event. See §15.
EventStorage(name, location, *options, fallback=None, fallback_text="There is nothing to do here.")A pool of events for a location. Holds three priority buckets internally. fallback
is the Event run when nothing is available; fallback_text is its "nothing to do"
line. register_highlighting(*storages) opts a pool into the map "available event"
highlight. add_storage(dict, storage) indexes a storage under its name in a dict.
A building typically builds a dict of per-action pools plus a "general" pool:
cafeteria_events = {}
add_storage(cafeteria_events, EventStorage("look_around", "cafeteria", fallback_text="Nothing to see here."))
add_storage(cafeteria_events, EventStorage("order_food", "cafeteria", fallback_text="I'm not hungry."))
cafeteria_general_event = EventStorage("cafeteria_general", "cafeteria",
fallback=Event(2, "cafeteria.after_general_check"))
register_highlighting(cafeteria_general_event)FragmentStorage(name, *options) # location fixed to "fragment"Holds EventFragments for a composite. A FragmentRepeatOption(number, repeatable)
controls how many fragments the composite pulls from it and whether the same fragment
may repeat — number may be a Selector for a random count:
truth_or_dare_storage = FragmentStorage("truth_or_dare",
FragmentRepeatOption(RandomValueSelector("", 2, 6, True), False)) # 2–6, no repeatsAfter the label name, an event takes any mix of four element types (the constructor sorts them):
-
Conditions — gates; the event is available only when all pass (§5). Compose
with
AND/OR/NOT(Conditions). AFlagCondition(None)is auto-added unless aFlagConditionappears anywhere in the conditions, also nested inAND/OR/NOT(§5, Event flags). ALevelConditionalso registers the event under its max level for level-based bookkeeping. -
Selectors — dynamic values rolled when the event runs, exposed under their key
(Selectors). They can be weighted/conditional:
RandomListSelector('topic', (0.7, 'apron'), (0.2, 'breasts'), 'nude'). -
Options — flags parsed specially:
PriorityOption(n)sets priority,ForceHighlightOption()forces highlight eligibility,ReplayCategoryOption(cat)sets the gallery category,EventSeenDebuffOption(x)lowers re-selection weight once seen. Others are stored on the event (Options). - Patterns — named, mod-redirected image patterns (§9).
truth_or_dare_event_1 = Event(3, "truth_or_dare_1",
TimeCondition(daytime="n"),
LevelCondition("2,3", char_obj="school"),
NOT(ProgressCondition("truth_or_dare")),
ReplayCategoryOption("truth_or_dare"),
Pattern("main", base_path + "truth_or_dare_1/truth_or_dare_1 <school_level> <step>.webp"),
thumbnail=base_path + "truth_or_dare_1/truth_or_dare_1 6 2.webp")LevelCondition("2,3", char_obj="school") is a climate gate: only fire this
scene while the campus is at those school levels. Put
<school_level> in the pattern so the stills match.
An event is available when every condition passes. Among available events in a
pool, select_type / priority decides what runs:
| Priority | Behavior |
|---|---|
| 1 | highest — the first available 1 runs and blocks all others (forced story beats) |
| 2 | all available 2s run in sequence |
| 3 | one is chosen at random among available 3s (the sandbox default) |
Most ambient events are 3; a story interrupt is 1. Highlighting: priority-3 events
don't light up the map unless they carry ForceHighlightOption();
is_highlighted() combines availability, priority and the options'
check_options(Highlight=True, …).
One global flag (GameData key current_flag) decides which events may run at all.
It is empty (None) by default. Set it for a special phase, e.g. the intro or the
beach camp, and only events that carry a matching FlagCondition stay available.
| Event has … | flag None
|
flag = "camp"
|
flag = other key |
|---|---|---|---|
no FlagCondition (→ auto FlagCondition(None)) |
✅ | ❌ | ❌ |
FlagCondition("camp", exclusive=False) |
✅ | ✅ | ❌ |
FlagCondition("camp") (exclusive=True, default) |
❌ | ✅ | ❌ |
FlagCondition("x") (wildcard) |
✅ | ✅ | ✅ |
# runs only during the camp
Event(2, "camp_campfire_talk",
FlagCondition("camp"),
TimeCondition(daytime = "6-7"))
# runs normally AND during the camp
Event(3, "kiosk_snack_run",
FlagCondition("camp", exclusive = False))-
How it's checked:
Event.__init__searches the conditions withfind_by_type("flag_condition")(the conditions live in aConditionStorage,event.conditionsis a read-only property onto it). If none is found, it appendsFlagCondition(None). The flag then goes through the normal condition check; there is no extra code path. -
Fragments are exempt.
EventFragments never get the automatic condition. The decision happened on the composite, so a camp composite needsFlagCondition("camp")but its fragments don't. -
EventSelectis not exempt. The select and every event in itsevent_listare normal events. A camp select needs the flag on the select and on each option. -
Directly called events bypass it. An
EventEffectwith an event key or anEventobject callsevent.call()without checking conditions (situation threshold and resolve scenes). Only anEventEffecton anEventStoragegoes through availability. This is intended: a situation whose flag doesn't match pauses on its own (Situation flags), so its scenes don't fire; otherwise put a condition on the threshold/resolution itself. -
Situation pool events without their own FlagCondition get
SituationFlagCondition(situation_key)instead ofFlagCondition(None): they follow their situation's flag. -
set_current_flag()also pauses/continues situations to match the new flag (Situation flags). -
Wildcard
"x", like elsewhere in the codebase. -
Helpers (
helper.rpy):set_current_flag(key)(Noneclears),get_current_flag(). One flag at a time; setting a new one replaces the old.
The intro flag. The intro is the first user of the system and replaces the old
date-based IntroCondition. update_intro_flag() sets "intro" before 10 January 2023
and clears it afterwards. It never touches another flag. It runs in label start
(before call intro), in after_load (migrates old saves) and at the top of
time_event_check, so the first morning after the intro already runs without the flag.
It sits in time_event_check and not in new_day because a day can also change in
new_daytime (night → morning).
Intro-only events use FlagCondition("intro"); map_tutorial uses
FlagCondition("intro", exclusive=False) so it can run during the intro and afterwards.
The parameter override_intro was removed.
Build events in an init python block (set the mod first), then add them to pools:
init 1 python:
set_current_mod('base')
# … build event objects …
cafeteria_events["order_food"].add_event(cafeteria_event_1, cafeteria_event_2)
cafeteria_general_event.add_event(cafeteria_construction_event)The location's entry label runs the pools — a "general" pass first, then the action menu:
label cafeteria():
call call_available_event(cafeteria_general_event) from cafeteria_4
label .after_general_check(**kwargs):
call call_event_menu("What to do at the Cafeteria?", cafeteria_events,
default_fallback, character.subtitles, **kwargs) from _cafeteria_menuThe label name is the building key: clicking the building on the map runs it. Registering the building and putting it on a map is covered in Maps.
call_available_event(storage, priority=0, no_fallback=False, **kwargs) runs the
available events of a storage; call_event_menu(text, events, fallback, person, **kwargs) renders the per-action choice menu. set_current_mod before building
Patterns redirects their paths into the active mod's folder (§17).
Every scene is a Ren'Py label named exactly like the event, bracketed by two calls:
label truth_or_dare_2(**kwargs):
$ begin_event(**kwargs)
# … the scene …
$ end_event("new_daytime", **kwargs)-
$ begin_event(version="1", **kwargs)— call it first (after any pre-scene choices). It hides leftover screens and (on a fresh scene, not a fragment or decision re-entry) clears the master layer, including the map overview's map image (tagmap_image, see Maps) — that image is not a screen, sohide_all()alone would leave it covering a paperdoll background at zorder-100. It stops sound, blocks rollback (locking in rolled selector values and choices), marks the event seen (set_event_seen), starts aGallery_Managerfor replay (unlessno_gallery=Truein kwargs), andinit_paperdoll_manager(). It also bindsheadmasterandemiko(the two ever-present speakers) so the scene can use them without a per-event$ ... = Person[...]— see Dialogue. Bumpversionwhen a scene's structure changes so stale replays are invalidated. -
$ end_event(return_type="new_daytime", **kwargs)— call it last."new_daytime"advances to the next daytime segment;"map_entry"returns to the map; in replay it returns to the journal. It also ticks the situation manager (teasers, thresholds, passives, resolutions), clears progress blocks, andunload_paperdoll_manager()(so paperdolls never leak past the event).
Never hand-roll the seen/gallery/rollback hooks — always bracket with these.
Branches are .-prefixed sub-labels (label .peek_1(**kwargs):) reached from the
decision menu (§12) or call/jump. Each branch that is entered independently should
also begin_event/end_event if it is a distinct scene.
Selector values arrive in kwargs["values"]. Read them through the gallery-aware
getters so they are recorded for replay:
| Getter | Returns |
|---|---|
get_value(key, alt=None, **kwargs) |
the rolled selector value (and registers it for replay) |
get_value_ng(key, alt=None, **kwargs) |
same, without registering (transient) |
get_stat_value(key, ranges, alt=100, **kwargs) |
a stat-selector value normalized into ranges
|
get_level(key, **kwargs) |
a character's level (replay-safe) |
get_kwargs(key, alt, **kwargs) |
a raw kwarg (not a selector value) |
$ topic = get_value("topic", **kwargs)
$ level = get_stat_value("corr", [0, 40, 70, 100], **kwargs)Prefer get_value over reaching into kwargs["values"] directly — the registration is
what makes the scene replayable. <key> placeholders in image paths are filled from
the same values automatically (§9), so you only call the getters for values you use
in Python/dialogue logic.
Images are declared as patterns on the event and shown from the label. The
resolver underneath (placeholders, $ alternatives, PNG/WebP, mod prefixes) is
documented in Images. This section is the event-authoring surface.
Pattern(key, path, *alternative_keys) names an image pattern. path contains
<placeholder>s filled from selector values and built-in keys (<step>,
<school_level>, <parent_level>, <variant>, <nude>, and any selector key). An
event can hold several named patterns ("main", "base", "card", …). Patterns are
mod-redirected (built while set_current_mod points at a mod).
$ show_pattern("main", **kwargs) # resolves & shows the pattern's imageUse this for a static scene image.
For a scene that advances through numbered frames, turn the pattern into an
Image_Series and drive its <step>:
$ image = convert_pattern("main", **kwargs) # -> Image_Series from the "main" pattern
$ image.show(0) # show step 0
"Dialogue over frame 0."
$ image.show(1) # advance to step 1-
convert_pattern(pattern_key, **kwargs)builds the series (its<key>s already filled fromvalues). -
image.show(step, display_type=SCENE, variant=-1)shows a specific<step>. -
convert_pattern(pattern_key, {"key": value}, **kwargs)builds a series with an extra/overridden substitution (e.g. force<girls>to a specific character for one image). The optionaldatadict is injected into thevalueslayer, so it never collides with an existing kwargs key — pass it here instead of as a top-level named parameter. For the inline named-argument form useconvert_pattern("main", **with_values(kwargs, girls="ikushi_ito")). - The
Image_Series.show_image(image, *steps)label plays several steps in sequence (advancing on click) — handy for a short run of frames with no dialogue between.
<step> and <nude> are resolved by the image system; a <nude> image shows the
highest available nude level for the current state.
A video is the animated version of a step: same stepped Pattern as a static
scene (§9), but each frame also has a looping Movie, and you play it with
image.show_video(...) instead of image.show(...). The image system finds the
Movie by a name convention derived from the step's .webp path, so you don't
reference it directly.
The event's pattern points at the .webp frames exactly as for a stepped scene:
Pattern("main", "/images/events/teaching/pe/warm_up/warm_up_1/teaching_pe_warm_up_1 <school_level> <step>.webp")For every frame that has video, declare a Movie image whose name is
anim_ + the resolved .webp filename, spaces replaced by underscores. That is
exactly the key show_video builds, so the names must line up:
define anim_gtpewu1_path = "/images/events/teaching/pe/warm_up/warm_up_1/teaching_pe_warm_up_1 "
# webp ".../teaching_pe_warm_up_1 1 0.webp" -> image name "anim_teaching_pe_warm_up_1_1_0"
image anim_teaching_pe_warm_up_1_1_0 = Movie(play = anim_gtpewu1_path + "1 0.webm", start_image = anim_gtpewu1_path + "1 0.webp", loop = True)
image anim_teaching_pe_warm_up_1_1_1 = Movie(play = anim_gtpewu1_path + "1 1.webm", start_image = anim_gtpewu1_path + "1 1.webp", loop = True)
# … one per <school_level> × <step> (× variant) that has a clip …-
play— the.webm.start_image— the matching.webp, shown until the video is ready and as a fallback if it can't play (always ship it).loop=Truekeeps the clip running. - The derived name is
video_prefix + basename(webp, no ext).replace(" ", "_"), withvideo_prefixdefaulting to"anim_"(overridable via avideo_prefixkwarg). So theMovieimage name must equalanim_+ the same tokens the<...>placeholders resolve to.
label gym_teach_pe_warm_up_1(**kwargs):
$ begin_event("2", **kwargs)
$ image = convert_pattern("main", **kwargs) # same Image_Series as for statics
$ image.show_video(0, True) # play step 0's clip, wait for click
$ image.show_video(1, True)
$ image.show_video(2, True)
headmaster "Alright, that's enough."
call change_stats_with_modifier('pe', charm=SMALL, education=TINY) from _e_pe
$ end_event('new_daytime', **kwargs)image.show_video(step, pause=False, variant=-1):
- resolves the step's
.webp(from the pattern,<school_level>/<step>/… filled), derives theanim_…name, and shows thatMovie; -
pause=Truewaits for a click before returning (so consecutiveshow_video(0, True); show_video(1, True)play one clip per click);pause=Falseshows and continues; -
variant=-1picks a random variant, likeimage.show.
Use image.show(step) for the static frame and image.show_video(step, …) for its
animated version — both come from the same convert_pattern("main") series, so a
scene can freely mix stills and clips.
Dialogue uses Ren'Py say statements with a Character object.
Keep the Person object and speak through its speech-mode properties — you get
dialogue and access to the rest of the person (name, level, …) from one variable, no
second definition:
$ miwa = Person["miwa_igarashi"]
miwa.say "That was crazy..."
miwa.think "...why did she do that?"
miwa.whisper "Don't tell anyone."
miwa.shout "Watch out!"
"[miwa.get_full_name()] blushes." # same object, other membersPerson["key"] returns the Person; each property returns a fresh Ren'Py
Character in a speech mode, so a say statement can use it directly (Ren'Py evaluates
the who-expression, and dotted/subscript expressions are allowed — even
Person["miwa_igarashi"].say "..." works inline):
| Property | Mode |
|---|---|
.say |
normal speech |
.think |
inner monologue — italic, parenthesized, suffix "(thinking)" |
.whisper |
italic, suffix "(whispering)" |
.shout |
bold, suffix "(shouting)" |
The older $ miwa = Person["miwa_igarashi"].get_renpy_char(char_type="") form still
works and returns the same Character (char_type ∈ "" / "thought" / "whisper"
/ "shout") — but the property form avoids the double get_renpy_char() definition
some events still carry. Each property access builds a new Character; that's cheap
and behaves identically (same name, retain=False).
The player (Headmaster) and the secretary (Emiko) are full Persons too, and
begin_event binds them for you — no $ headmaster = Person[...] needed. The
Headmaster has a player-chosen, save-specific name (resolved live), so his def carries
no name; Emiko picks up the secretary look automatically:
$ begin_event(**kwargs) # binds `headmaster` and `emiko`
emiko "Anything urgent this morning?"
headmaster.think "Same plaque, still 'in process.'"
headmaster "Chase it today."Per-character name-tag colour, text colour, font, size and format can be set on the
Person itself with styleOverrides instead of the shared category Character.
Neutral narration voices are also available directly — character.subtitles
(subtitle voice), character.dev (developer notes).
The full speaker/styling model —
CharvsPerson, the styling categories,styleOverrides, and the Headmaster/Emiko name handling — is documented in Dialogue.
When a selector picked the character (its value is a character key), resolve it to a speaking character:
$ girl = get_person_char("girl_name", **kwargs) # selector "girl_name" -> Character
girl "Fine, I'll do the dare."-
get_person_char(key, alt=None, **kwargs)→ a Ren'PyCharacterfor the selected key (handles theschool/parent/teacher/secretaryroles and falls back to a default view). Use it when you only need the speaking character. -
get_person_value(key, alt=None, **kwargs)→ the resolvedPersonobject — so you get the same speech-mode properties as a fixed character and the person's other members:$ girl = get_person_value("girl_name", **kwargs) girl.say "Fine, I'll do the dare." girl.think "...this is embarrassing."
-
get_person_char_with_key(group_key, name, char_type="")→ resolve a character from a collection group plus a name.
This is why an event stays generic: the selector chooses who, and
get_person_value / get_person_char turn that choice into the voice that speaks.
Player choices inside an event go through a purpose-built decision menu, not a
plain Ren'Py menu:. It records each choice into the gallery so the scene can be
replayed (and, in replay, only the originally-possible branches are offered).
label truth_or_dare_4_choice(**kwargs):
call call_custom_menu_with_text(
"Truth or Dare?", character.subtitles, True,
MenuElement("truth", "Pick Truth", EventEffect(".truth_branch")),
MenuElement("dare", "Pick Dare", EventEffect(".dare_branch")),
MenuElement("peek", "Keep watching", EventEffect(".peek"),
StatCondition(corruption=20)), # gated choice
**kwargs
) from _tod_choice-
call_custom_menu_with_text(text, person, with_leave, *MenuElement, **kwargs)— a decision menu with a prompt line.call_custom_menu(with_leave, *elements, **kwargs)is the same without prompt text.with_leaveadds a "leave" option. -
MenuElement(key, title, *data, active=True, overwrite_position=None)— one choice.keyis the decision key recorded for replay (keep it stable and unique within the menu).titleis the displayed label.*datamay include:-
Effects to run when chosen — most oftenEventEffect("<label>")to jump into a branch (a string label works too); -
Conditions (or a barebool) that gate whether the choice appears.
-
By default a decision menu sits in the middle-center of the screen. Pass a
menu_anchor to place it elsewhere — e.g. keep it clear of a paperdoll character
standing on one side:
$ emiko.display(PDAMove(alignX=-0.2, duration=1.0)) # move her to the left
$ call_custom_menu_with_text("What do you do with the nameplate?", character.subtitles, False,
MenuElement("fix", "Chase the right plaque", EventEffect(".fix")),
MenuElement("charm_fix", "Draft the order over a coffee", EventEffect(".charm_fix"), high_charm),
MenuElement("leave_it", "Leave the taped printout", EventEffect(".leave_it")),
menu_anchor=MENU_ANCHOR_MIDDLE_RIGHT, # menu hugs the right, away from her
**kwargs)-
menu_anchoris a keyword arg on bothcall_custom_menuandcall_custom_menu_with_text(defaultMENU_ANCHOR_MIDDLE_CENTER). Use the named constants — one per point of a 3×3 grid:left center right top MENU_ANCHOR_TOP_LEFTMENU_ANCHOR_TOP_CENTERMENU_ANCHOR_TOP_RIGHTmiddle MENU_ANCHOR_MIDDLE_LEFTMENU_ANCHOR_MIDDLE_CENTERMENU_ANCHOR_MIDDLE_RIGHTbottom MENU_ANCHOR_BOTTOM_LEFTMENU_ANCHOR_BOTTOM_CENTERMENU_ANCHOR_BOTTOM_RIGHT -
Dialogue-aware: when the menu carries a prompt line (a textbox is open), middle and bottom anchors are pushed up so the menu sits above the dialogue box instead of behind it; when there's no dialogue they use the full screen height. Top anchors always hug the top. You don't manage this — the menu reads
has_dialogand reserves the space itself. -
The anchor rides along in
kwargs(apply_menu_anchor), so it survives across the internal menu entry points and pagination without you re-passing it. -
MenuElement's ownoverwrite_position=(x, y)still pins an individual choice to absolute coordinates; that's independent ofmenu_anchor, which lays out the menu as a whole.
call_element registers the decision (register_decision(key) outside replay; appends
to made_decisions in replay), then runs the element's effects (a label is called, an
Effect is applied). So a choice both branches the scene and records itself.
In replay, the menu is filtered by get_decision_possibilities(...) to the decisions
that were actually recorded, so the gallery reproduces the original branch set. This is
why you must use the decision menu (not a raw menu:) for any in-event choice you want
in the gallery — and why decision keys must stay stable across versions (bump the
event version in begin_event if you restructure them).
Change school stats through the modifier path so global modifiers apply:
call change_stats_with_modifier(happiness=SMALL, inhibition=DEC_SMALL) from _e3
# with an explicit collection/context (e.g. a subject or location):
call change_stats_with_modifier('pe', happiness=MEDIUM, charm=SMALL) from _e4- The optional first positional is the modifier collection (default
'default'; e.g.'pe','cafeteria') — it selects whichdefault-collection modifiers reshape the change (Modifiers). - Amounts are named constants —
TINY,SMALL,MEDIUM,LARGEand theirDEC_decrement forms (DEC_TINY,DEC_SMALL, …). Use these, not magic numbers. - These changes also auto-drift active situations via their
stat_weights, and a situation bar can be pushed directly with asituation:<key>:<bar>key (Building Situations §13).
change_stats_with_modifier applies a one-shot change at event time. To install a
lasting modifier from an event — a drift/buff that stays registered after the scene
ends — attach a ModifierSelector (Selectors) and activate it in the label:
# in the definition:
Event(3, "cafeteria_regulars",
ModifierSelector("regulars_bonus", Modifier_Obj("regulars_bonus", "+", 2),
HAPPINESS, collection="daily"),
...)
# in the scene label, where the modifier should take effect:
$ load_modifier("regulars_bonus", **kwargs)load_modifier does two things: it applies the modifier and registers it with
the lifecycle registry, owned by this event (registry key "<event>:<selector_key>").
You do not manage its removal:
- Every load wave the event system runs
check_selectors()on each registered event, which KEEP-pings that event'sModifierSelectormodifiers — so the modifier survives. - If the event ever stops being registered (e.g. its mod is removed, so its definition never runs), nothing re-affirms the modifier and the next lifecycle sweep removes it — no orphan. This is the managed-modifier model in Modifiers.
-
load_modifieris a no-op in replay and when noevent_nameis in context.
Use this only for modifiers meant to outlive the event. For an immediate stat change,
stay with change_stats_with_modifier; for character-scoped or one-off effects use the
Effects.
Event-series progress tracks multi-part storylines:
$ start_progress("truth_or_dare") # begin (progress 0/started)
$ set_progress("truth_or_dare", 2) # set a stepGate later events on it with ProgressCondition("truth_or_dare", 2) and read it with a
ProgressSelector.
For character stats or one-off actions, use the Effects (StatEffect,
MoneyEffect, …) rather than the school-stat modifier path.
A composite is a scene whose middle is a shuffled run of fragments.
Definition — an EventComposite referencing one or more FragmentStorages, each
filled with EventFragments:
truth_or_dare_storage = FragmentStorage("truth_or_dare", FragmentRepeatOption(RandomValueSelector("", 2, 6, True), False))
truth_or_dare_end_storage = FragmentStorage("truth_or_dare_end")
truth_or_dare_4 = EventComposite(3, "truth_or_dare_4",
[truth_or_dare_storage, truth_or_dare_end_storage],
TimeCondition(weekday="d", daytime="n"),
ProgressCondition("truth_or_dare", 3),
IterativeListSelector("girls", "ikushi_ito", "lin_kato", "miwa_igarashi", options=[FragmentRerollOption()]),
Pattern("base", base_path + "…/main <school_level> <step>.webp"),
Pattern("card", base_path + "…/card <girls> <school_level> <step>.webp"))
truth_or_dare_storage.add_event(
EventFragment(2, "truth_or_dare_truth_1", LevelCondition("2-10"), ReplayCategoryOption("truth_or_dare"),
Pattern("main", base_path + "…/truth_1 <school_level> <step>.webp")),
# … more truth/dare fragments …
)
truth_or_dare_end_storage.add_event(
EventFragment(2, "truth_or_dare_end", ReplayCategoryOption("truth_or_dare"), Pattern("main", …)))Flow — the composite's own label plays an intro, then hands off:
label truth_or_dare_4(**kwargs):
$ begin_event(**kwargs)
$ image = convert_pattern("base", **kwargs)
$ image.show(0)
ishimaru "Okay, let's start! Pull a card."
call composite_event_runner(**kwargs) from _tod4_run # selects & plays fragmentscomposite_event_runner calls select_fragments (which honors each storage's
FragmentRepeatOption — count and repeatability) and plays the chosen fragments in
order via call_fragment, then end_events. Each fragment is its own label
(begin_event/scene/end_event).
Notes:
- A
SelectormarkedFragmentRerollOption()(likegirls) is re-rolled between fragments, so each fragment can feature a different pick. - Fragment patterns are exposed as
frag_image_patterns;convert_pattern/show_patterninside a fragment resolve against them. - The composite's own patterns (
base,card) remain available during its intro/outro.
EventSelect presents a menu that routes into other pools — use it when one map action
should offer a sub-choice of storages:
peek_select = EventSelect(3, "dorm_peek_select", "Which room do you peek into?",
{ "left": dorm_left_events, "right": dorm_right_events },
TimeCondition(daytime="n"),
override_menu_exit="map_entry", person=character.subtitles)Its label is select_event_runner, which renders call_event_menu(text, event_list, fallback, person, …) and, after the chosen sub-event, jumps to override_menu_exit.
It is available only if some storage in event_list has an available event, and it
highlights if any of them have a highlightable event.
begin_event records the event as seen and (outside replay) starts a
Gallery_Manager that captures the rolled values and decisions, so the scene can be
replayed from the journal gallery. Implications for authors:
-
Seen state is queryable:
EventSeenCondition(True, "event_name")/get_event_seen(...)— this is how later content reacts to what the player saw. -
Read values via
get_valueand choose via the decision menu — both register into the gallery; anything read/chosen another way won't replay correctly. -
Conditions pass in replay (the base
Condition.is_fulfilledreturnsTruewhenin_replay/gallery), so replays play straight through — don't rely on a gate to skip scene-critical code. -
Version your scenes:
begin_event(version="2", **kwargs)invalidates stale galleries when you restructure a scene's steps or decisions. -
ReplayCategoryOption("category")groups a storyline's events/fragments together in the gallery (e.g. all Truth or Dare fragments under"truth_or_dare").
Events are fully moddable:
init 1 python:
set_current_mod('my_mod') # redirect Pattern/thumbnail paths into the mod
my_event = Event(3, "mymod_scene", TimeCondition(daytime="d"),
Pattern("main", "images/events/mymod_scene/<step>.webp"),
thumbnail="images/events/mymod_scene/1.webp")
cafeteria_events["look_around"].add_event(my_event) # extend a base pool …-
set_current_mod(key)at the top makesPatterns and the thumbnail resolve into your mod folder — write plain paths relative to your mod root, nevermods/MyMod/.... - You can add events to base pools or your own, gate on base state, and be gated by it — coupling is only through keys, conditions and progress.
- The mod's event count is tracked (
change_mod_event_count).
-
Definition name = label name. A mismatch means "available event, missing label" —
check_eventlogs it and the event is invalidated. -
Always bracket scenes with
begin_event/end_event; branches that are distinct scenes get their own pair. -
Gate with conditions, vary with selectors. Don't hard-code the time, character or
variant — select them and reference
<key>in the pattern. -
Read values with
get_value(not rawkwargs["values"]) and branch with the decision menu (not a rawmenu:) so scenes replay correctly. -
Use the amount constants and
change_stats_with_modifierfor stat changes; use progress for storyline gating. -
Keep decision
keys and selector keys stable; bump the eventversionwhen you restructure steps/decisions. -
Set the mod context before building
Patterns so images redirect. -
Ship a
start_imagenext to everyMovie. -
Priority intent:
3ambient,2always-run,1blocking interrupt. -
Match the school level. Dialogue, stills (
<school_level>), and paperdolllevel=must agree with the climate in School Levels. Gate withLevelCondition; if the event spans a wide range, branch the text.
| Symptom | Likely cause | Fix |
|---|---|---|
| "Nothing to do here" when you expect an event | No event's conditions pass, or none registered in that pool | Check gates (time/level/progress); confirm add_event into the right pool. |
| Crash on firing | Label name ≠ Event string, or label missing |
Make them identical; define the label (see the event log category). |
| Event never runs during the intro / camp | The flag is set and the event has no matching FlagCondition (it got the automatic FlagCondition(None)) |
Add FlagCondition(key) or FlagCondition(key, exclusive=False) (§5, Event flags). Check get_current_flag(). |
Image shows a literal <key>
|
Selector missing/misnamed, or read outside the pattern | Add the selector; match the key; for logic use get_value. |
| Value differs between image and dialogue | Read via raw kwargs, or a realtime selector re-rolled |
Read once with get_value; keep the selector cached (§8, Selectors). |
| Choice missing in replay | A raw menu: was used, or the decision key changed |
Use call_custom_menu_with_text + stable MenuElement keys; bump version. |
| Mod images not found |
set_current_mod not set before the Pattern was built |
Set the mod context at the top of the init block. |
| Composite plays nothing | Empty FragmentStorage, or no fragment's conditions pass |
Add fragments; loosen their gates; check the event log. |
| Story event won't interrupt | It's priority 3 (random) not 1 (blocking) | Use select_type=1. |
| Situation event never appears | Missing/incorrect pool condition or bar out of range | Match the SituationPoolCondition and the SituationPool range (Building Situations). |
| Paperdoll-only event still shows the campus map | the map is an image (show … as map_image), not a screen |
begin_event now scene()s it away; set_background also hides map_image. Don't skip begin_event. |
Event(select_type, event, *conditions|selectors|options|patterns, thumbnail="", register_self=True, override_location=None) ·
EventComposite(priority, event, fragments, *conditions, thumbnail="") ·
EventFragment(select_type, event, *conditions, thumbnail="") ·
EventSelect(priority, event, text, event_list, *conditions, thumbnail="", override_menu_exit="map_entry", fallback=None, person=None) ·
EventStorage(name, location, *options, fallback=None, fallback_text="…") ·
FragmentStorage(name, *options).
label <event>(**kwargs): → $ begin_event(**kwargs) … $ end_event("new_daytime", **kwargs).
get_value(key, alt, **kwargs) · get_value_ng(...) · get_stat_value(key, ranges, alt, **kwargs) ·
get_level(key, **kwargs) · get_kwargs(key, alt, **kwargs).
Pattern(key, path, *alt_keys) · show_pattern(key, **kwargs) ·
convert_pattern(key, data=None, **kwargs) → Image_Series · image.show(step, display_type, variant) ·
with_values(kwargs, **values) (inject into the values layer, collision-free) · Image_Series.show_image(image, *steps).
Video: image.show_video(step, pause=False, variant=-1) with a
Movie(play=<webm>, start_image=<webp>, loop=True) image named anim_ + the step's
webp basename (spaces → underscores).
Person["key"] → .say / .think / .whisper / .shout (or .get_renpy_char(char_type)) ·
get_person_value(key, alt, **kwargs) → Person (same properties) ·
get_person_char(key, alt, **kwargs) → Character · get_person_char_with_key(group, name, char_type) ·
headmaster_thought · character.subtitles · character.dev.
call_custom_menu_with_text(text, person, with_leave, *MenuElement, **kwargs) ·
call_custom_menu(with_leave, *elements, **kwargs) ·
MenuElement(key, title, *effects|conditions|bool, active=True, overwrite_position=None) ·
register_decision(key) (called for you by call_element).
Menu placement: menu_anchor= (default MENU_ANCHOR_MIDDLE_CENTER) — nine constants
MENU_ANCHOR_{TOP,MIDDLE,BOTTOM}_{LEFT,CENTER,RIGHT}; middle/bottom lift above the
dialogue box when a prompt line is shown.
call change_stats_with_modifier('<collection>', stat=AMOUNT, …) · amounts TINY/SMALL/MEDIUM/LARGE (+ DEC_*) ·
start_progress(key) / set_progress(key, n) · ProgressCondition / ProgressSelector.
call_event · call_available_event · call_event_menu · select_event_runner ·
composite_event_runner.
-
game/scripts/event.rpy— event classes, storages,begin_event/end_event, runners -
game/scripts/menu.rpy—MenuElement,call_custom_menu*,call_element(decisions) -
game/scripts/gallery.rpy—get_value,register_decision, replay/gallery -
game/scripts/images.rpy—Pattern,Image_Series,convert_pattern,show_pattern -
game/scripts/character.rpy—Person,get_person_char* -
game/scripts/buildings/*.rpy— real pools & definitions (e.g.cafeteria.rpy) -
game/scripts/events/*.rpy— real scene labels (e.g.truth_or_dare.rpy,teaching_lessons.rpy) - Images · Conditions · Selectors · Effects · Modifiers · Options · Building Situations · School Levels
Mind the School · This wiki is maintained in the main repo's wiki/ folder — do not edit pages directly here.
🎮 Players
Walkthroughs
Characters
Students (Class 3A)
- Aona Komuro
- Easkey Tanaka
- Elsie Johnson
- Gloria Goto
- Luna Clark
- Seraphina Clark
- Hatano Miwa
- Ikushi Ito
- Ishimaru Maki
- Kokoro Nakamura
- Lin Kato
- Miwa Igarashi
- Sakura Mori
- Soyoon Yamamoto
- Yuriko Oshima
Teachers
Staff
Parents
🎛️ Tools
🛠️ Developers