Selectors - SuIT-pub/Mind-the-School GitHub Wiki
Audience: Developers writing Mind the School events who need dynamic values โ a random outfit variant, the acting character, a stat reading, a time-of-day โ computed at runtime and woven into image paths, dialogue, effects and conditions.
Scope: The selector system (
selector.rpy). Selectors are an event-authoring tool: they produce values that land in the event'skwargs. They pair with Events (which run them), and some Conditions / Effects accept aSelectorin place of a fixed value.
A Selector computes a value under a key; that key becomes a kwarg the rest of the event can read:
SelectorSet(
CharacterSelector("char"), # pick the acting character โ kwargs["char"]
RandomValueSelector("variant", 1, 4), # random 1..4 โ kwargs["variant"]
StatSelector("corr", CORRUPTION, "char", [0, 100]), # a stat reading (+ "corr_range")
)Downstream you reference those keys:
- in image paths:
"images/scene/<char>/pose_<variant>.webp"(resolved byrefine_image/get_image); - in Ren'Py text:
"[char] looks away."; - as values passed into conditions/effects that accept a
Selector.
- What is a Selector?
- The base contract
- SelectorSet & how values reach kwargs
- realtime vs. cached, and rerolling
- Nesting selectors
- The selector catalog
- Using selector values downstream
- Conventions
- Troubleshooting
- Reference tables
A Selector produces a value at runtime and stores it in the event kwargs under
a key. That is how an event stays generic: instead of hard-coding "Yuriko" or
"variant 2", you declare how to pick and let the selector roll it when the event
runs. The result flows into image resolution, dialogue interpolation, and any
condition/effect that reads the kwarg.
Selectors are almost always attached to events (via a SelectorSet). They are a
content-authoring convenience, not a gameplay system of their own.
Every selector subclasses Selector(realtime, key, *options):
-
keyโ the kwarg name the value is stored under (get_name()returns it). -
roll(**kwargs) -> Any(abstract) โ compute a fresh value. -
get_value(**kwargs)โ return the value; rolls first ifrealtimeor if no value is cached yet. -
update(**kwargs)โ force a re-roll (caches the result). -
optionsโ trailing*options(OptionSet); see the Options guide (e.g.FragmentReroll).
You read values through get_value (or, in practice, through the SelectorSet);
never poke _value directly.
SelectorSet(*selectors) is the container an event holds. Its job is to turn the
selectors into kwargs:
-
get_values(**kwargs) -> dictโ merges every selector'skey: valueintokwargsand returns it (rolling once if not yet rolled). This is what feeds the event. -
roll_values(**kwargs)โ re-rolls all selectors and re-merges. -
add_selector(*selectors)โ extend the set.
Two selectors are merged specially:
-
KwargsSelectorcontributes multiple keys at once (not onekey: value). -
StatSelectorcontributes bothkeyandkey + "_range"(so downstream can scale against the stat's range).
-
Cached (
realtime=False, the default for most) โ the value is rolled once and stays fixed for the event, so an image path and the dialogue that reference the same key agree. -
Realtime (
realtime=True) โ re-rolled on everyget_value.StatSelectordefaults to realtime (a stat reading should be current); use realtime only when you truly want a fresh value each access.
reroll_selectors() re-rolls every selector queued in the global rerollSelectors
list โ used to refresh values across a parenting/fragment boundary so a follow-up
scene can pick new values.
Selectors compose. Wherever a selector takes a value, that value may itself be a
Selector, and it gets rolled recursively:
-
RandomListSelector("x", "a", "b", AnotherSelector(...))โ if the nested selector is chosen, it rolls and its result is used. - Many selectors accept
str | Selectorfor their target (e.g.StatSelector'sstat/char,LevelSelector'schar) โ so the character or stat to read can itself be selected dynamically.
This lets you build "pick a character, then read that character's stat" without hard-coding the link.
realtime defaults to False unless noted; *options accepted last.
| Constructor | Produces |
|---|---|
RandomListSelector(key, *values, realtime=False, alt=None, options=[]) |
a random pick from values (nested selectors roll; alt if empty) |
IterativeListSelector(key, *values, realtime=False, options=[]) |
the next value in order each roll |
RandomValueSelector(key, min_value, max_value, realtime=False, *options) |
a random integer in [min, max]
|
| Constructor | Produces |
|---|---|
StatSelector(key, stat, char, stat_range, realtime=True, *options) |
a character's stat value (+ key_range); stat/char may be selectors |
LevelSelector(key, char, *options) |
a character's level ("school" โ campus climate; see School Levels) |
CharacterSelector(key, char='school', *options) |
resolve a character reference |
TimeSelector(key, time_type, *options) |
a time component (day/daytime/โฆ) |
BuildingLevelSelector(key, building, *options) |
a building's level |
| Constructor | Produces |
|---|---|
ValueSelector(key, value, *options) |
a fixed value (wrap a constant as a selector) |
GameDataSelector(key, index, alt=None, *options) |
a GameData entry (alt fallback) |
DictSelector(key, index, dict, *options) |
a lookup into a provided dict |
KwargsValueSelector(key, kwargs_key, *options) |
copy another kwarg's value under key
|
ProgressSelector(key, index, *options) |
an event-series progress value |
NumClampSelector(key, value, *options, min_value=-1, max_value=-1) |
clamp a number/selector into a range |
| Constructor | Produces |
|---|---|
ConditionSelector(key, condition, true_value, false_value, realtime=False, *options) |
true_value if the Condition holds, else false_value (either may be a selector) |
KwargsSelector(*options, **kwargs) |
injects several fixed key=value kwargs at once |
| Constructor | Produces |
|---|---|
BuildingUnlockedSelector(key, building, *options) |
whether a map building is unlocked |
PTAVoteSelector(key, condition_type='misc', *options) |
a PTA vote value by category |
PTAObjectSelector(key, *options) |
the current PTA object under vote |
Legacy:
RuleUnlockedSelector/ClubUnlockedSelectortarget the retired rules/clubs system. Prefer reading unlock state via anUnlockableCondition(or aGameDataSelectoron the unlocked flag) instead.
| Constructor | Produces |
|---|---|
ModifierSelector(key, modifier, stat, *options, collection="default") |
a (modifier, stat, collection) triple for deferred modifier activation โ not an image/text value |
Unlike every selector above, a ModifierSelector's value is never substituted into art
or dialogue. It carries a Modifier_Obj that the event activates on demand by
calling load_modifier("key", **kwargs) in its scene label. load_modifier applies the
modifier and registers it with the lifecycle registry, owned by the event โ so it is
orphan-safe: kept alive while the event stays registered, swept if the event goes away.
Give each ModifierSelector on an event a distinct key (it becomes part of the
modifier's registry key "<event>:<key>"). Full flow: Events ยง13 and
Modifiers.
Once a selector has put key: value into the kwargs:
-
Image paths substitute
<key>โrefine_image/get_imagereplace every<key>with its kwarg value (this is how variants, characters and levels select the right art).StatSelector'skey_rangeis available for range-based art. -
Dialogue uses Ren'Py interpolation โ
"[key]". -
Conditions/effects that accept
Union[..., Selector](e.g.NumValueCondition,CompareCondition,NumClampSelector) can be handed a selector directly, or read the resulting kwarg by key.
Because everything travels as kwargs, a selector defined once drives art, text and logic consistently within the event.
-
One key, one meaning. A selector's
keyis a kwarg name; keep it descriptive and don't collide with kwargs the event system already sets (char_obj, event metadata, โฆ). -
Cache by default. Leave
realtime=Falseunless a value must be fresh on each read; otherwise the image and the dialogue referencing the same key can disagree. - Nest instead of duplicating. "Read the selected character's stat" = pass the character selector into the stat selector, don't roll the character twice.
-
Prefer
UnlockableCondition/GameData over the legacy rule/club selectors. -
StatSelectorgives you a range too (key_range) โ use it for range-scaled art rather than hard-coding bounds.
| Symptom | Likely cause | Fix |
|---|---|---|
Image/text shows a literal <key> / [key]
|
The selector isn't in the event's SelectorSet, or the key differs |
Add the selector; match the key exactly. |
| Value changes mid-scene unexpectedly | The selector is realtime (re-rolls each access) |
Set realtime=False to freeze it for the scene. |
| Same value every time you wanted variety | Cached and never re-rolled across the boundary | Use realtime, or rely on the reroll between parenting/fragments. |
StatSelector "_range" missing |
Reading the wrong key | It's key + "_range"; the SelectorSet adds it automatically. |
Nested selector returns a Selector object |
It wasn't rolled | Use the built-in nesting (list/target accepts Selector) so it rolls recursively; don't stuff a raw selector where a value is expected. |
Selector(realtime, key, *options) ยท abstract roll(**kwargs) ยท get_value (rolls
if realtime/uncached) ยท update (force re-roll) ยท get_name = key.
SelectorSet(*selectors) ยท get_values(**kwargs) โ kwargs dict ยท roll_values ยท
add_selector. Special: KwargsSelector (many keys), StatSelector (+_range).
Image paths: <key> (via refine_image/get_image) ยท Text: [key] (Ren'Py) ยท
Logic: pass a Selector where Union[..., Selector] is accepted.
-
game/scripts/selector.rpyโ all selector classes +SelectorSet -
game/scripts/event.rpyโ events that run selectors -
game/scripts/images.rpyโrefine_image/get_image(<key>substitution) - Images โ path resolution, PNG/WebP, mod prefixes
-
Conditions / Effects โ consumers that accept a
Selector