Repository navigation
Images
Audience: Developers writing Mind the School content who need to show a picture — a scene frame in an event, a thumbnail in a screen, a building sprite, a paperdoll layer, a journal icon. This page is the map of how a path string becomes a file on disk.
Scope: The image system (
images.rpy): patterns, placeholder substitution, alternative fallbacks, PNG/WebP compatibility, mod-path prefixing, and which helper to call from events, screens and classes. Layered talking sprites are covered in Paperdoll; event wiring that uses patterns is in Events.
Inside an event, declare a pattern and show it:
# at Event construction (init, after set_current_mod)
Pattern("main", "images/events/cafeteria/snack_chat/<topic> <step>.webp")
# in the scene label
$ image = convert_pattern("main", **kwargs)
$ image.show(0)
"Dialogue over frame 0."
$ image.show(1)Write plain paths (images/…). Never prefix mods/MyMod/ yourself — the mod
context does that. End the pattern in .png or .webp; the other extension is found
automatically if that is what exists on disk.
- What the image system is
- Path anatomy
- PNG / WebP compatibility
- Mod paths
- How a path is resolved
- Built-in placeholders
- Alternatives and the
$wildcard - Nude levels
- Events
- Screens
- Classes and other systems
- Background images
- Overwriting event images
- Videos
- Conventions
- Troubleshooting
- Reference
Almost every picture in the game starts as a path string with optional
<placeholders>:
images/events/cafeteria/snack_chat/<topic> <step>.webp
At display time the system:
- prefixes the mod folder if the object was built under a mod,
- replaces every
<key>with a runtime value (selector, level, step, …), - optionally tries
$wildcards for keys listed as alternatives, - picks the best available level when
<level>is in the path, - accepts
.pngor.webpregardless of which extension the pattern wrote, - shows the file that actually exists (or logs an error and shows nothing).
The engine does not care whether the caller is an event, a screen or a paperdoll layer. Those just pick a different entry point into the same pipeline.
A path is a string. Tokens in <angle brackets> are placeholders; everything else is
literal, including spaces.
| Piece | Example | Meaning |
|---|---|---|
| Folder | images/events/cafeteria/snack_chat/ |
relative to game/ (or the mod root — §4) |
| Name tokens | <topic> <step> |
filled from kwargs / selectors |
| Wildcard | $ |
"this dimension does not apply" — a real character in the filename |
| Extension |
.webp or .png
|
preferred format; the other is tried automatically |
Typical filenames on disk:
images/events/cafeteria/snack_chat/muffin 0.webp
images/events/cafeteria/snack_chat/muffin 1.webp
images/paperdoll/Emiko/bottom/Emiko $ 1 uniform 6 $ $.png
$ is not a placeholder. It is the character the game writes into the filename
when an alternative key is allowed to fall back (§7).
Patterns still name one extension. Resolution always tries that one first, then the other:
| Pattern says | File on disk | Result |
|---|---|---|
….png |
….png |
….png |
….png |
….webp |
….webp |
….webp |
….webp |
….webp |
….webp |
….png |
….png |
….png |
both |
….png (the pattern wins) |
So you can keep writing .png in paperdoll patterns and ship WebP assets, or mix
formats in one folder. The returned / displayed path is the file that exists.
This is implemented once, in find_loadable_image, and used by:
-
refine_image/refine_image_with_alternatives/refine_image_with_variant -
find_available_images/check_image -
get_image/get_available_level/get_image_max_value* -
Image_Series(step/variant probing and display) -
show_ready_imageand the nude-toggle screens - paperdoll layer and background resolution
You do not write extension fallbacks yourself. Do not call renpy.loadable on a
pattern path and then add that same string — if the file is the other format, the
check would pass (via check_image) but the original string would not load. Resolve
first, display the resolved path:
$ path = find_loadable_image(some_path)
if path:
add pathset_current_mod('mymod_key') at the top of an init / loader block does two things
(see Modding): it gates registration on the mod being enabled, and it
redirects assets.
While that context is active, constructing any of these prepends
get_mod_path(active_mod_key) (e.g. mods/MyMod/) to the path you wrote:
| Object | When the prefix is applied |
|---|---|
Pattern |
constructor — stored on _pattern
|
Person |
constructor — self.basePath; paperdoll patterns and portraits use it |
Building |
constructor — self.image
|
Map |
constructor — self.map_path
|
Situation / threshold / teaser |
constructor — thumbnail / image
|
Pictogram |
constructor — icon
|
ItemData |
constructor — self.image
|
BGImage inside BGStorage
|
set_path_prefix(...) when the storage is built |
Write plain paths, the same as the base game:
set_current_mod('mymod_key')
Pattern("main", "images/mymod_scene <step>.webp")
# file lives at game/mods/MyMod/images/mymod_scene 0.webpNever write mods/MyMod/images/… yourself — that would double the prefix.
The base game does the same with set_current_mod('base'). For the base game the
prefix is empty, so images/… stays images/….
PNG/WebP swapping runs after the prefix is in place, so a mod file
mods/MyMod/images/foo.webp is found from a pattern that says images/foo.png.
Pattern.get_path() prepends get_mod_path(active_mod_key) again at call time.
The constructor already stored a prefixed _pattern, so this is a no-op when the
current prefix is empty (typical once inits have finished with 'base'). Do not "fix"
missing mod images by baking mods/… into the pattern string.
Two families of helpers, depending on whether you have one path or a list of fallbacks.
pattern + kwargs
→ replace <school_level> / <teacher_level> / … from the live stats
→ get_available_level for <level> (nearest existing level, png or webp)
→ replace every remaining <key> from kwargs
→ apply_available_image_extension (png ↔ webp if the path has no leftover <…>)
refine_image(path, **kwargs) returns that string. Paths that still contain
<placeholders> (e.g. <step>, <nude>) keep the original extension so later
probing can fill them in.
get_image(path, **kwargs) does the same substitution, then:
- if there is no
<nude>, returns(0, loadable_path)or(-1, original)on miss, - if there is
<nude>, returns(max_nude_level, template)where the template still holds<nude>but already has the extension of the file that exists.
Used when some keys are allowed to fall back to $:
pattern + alt_keys + kwargs
→ one candidate per combination of alt_keys replaced by $
→ same level / kwarg fill as refine_image
→ sort: fewer $ first (more specific wins)
→ apply_available_image_extension on complete paths
→ find_available_images: first candidate that loadable (png or webp)
Paperdoll layers and Image_Series both use this. You almost never call it by hand
from an event label.
The low-level show label. If the path still contains <nude>, it opens the nude
toggle UI. Otherwise it resolves png/webp and scene / shows the resolved path.
SCENE (default) replaces the background; SHOW adds it as general_image.
These keys are filled even if you did not pass them in kwargs:
| Placeholder | Filled from |
|---|---|
<school_level> |
school character's level — campus climate; dress/talk/behaviour in School Levels |
<teacher_level> |
teacher character's level |
<parent_level> |
parent character's level |
<secretary_level> |
secretary character's level |
<level> |
nearest existing file at or below the requested level, else the next one up |
<variant> |
random 1…max among files that exist (get_image / refine_image_with_variant) |
<nude> |
not replaced with a single value — see §8 |
<step> |
the frame index, filled by Image_Series.show(n)
|
<loli> / <loli_content>
|
content-filter values, injected if missing |
Any other <key> comes from kwargs: event selectors (Selectors),
values you pass into refine_image, paperdoll PDAImage keys, a building's
state=, and so on.
<level> is special: the system does not require a file for every level. It walks
down from the requested level, then up, and rewrites the path to the closest file
that exists (png or webp).
Some dimensions are optional on disk. A paperdoll body might have a per-level file
or a generic $ file. You list those keys as alternatives; the resolver
generates every combination, most specific first:
Pattern("main", "images/scene/<char> <outfit> <level>.webp", "level", "outfit")Candidates, in order (requested char=emiko, outfit=uniform, level=6):
images/scene/emiko uniform 6.webp-
images/scene/emiko uniform $.webp(level→$) -
images/scene/emiko $ 6.webp(outfit→$) -
images/scene/emiko $ $.webp(both)
The first that exists (as .webp or .png) wins.
An alternative key whose runtime value is empty ("", whitespace) or None is
not written into the filename as a blank token. Blank tokens are not valid
files, and a candidate with zero $ sorts as most specific — so interpolating
extra1="" would search for … .png and never reach … $.png. Empty/missing
alt-key values become $ immediately. A specific value (extra1="wet") is
still tried first, then $.
Paperdoll character layers use
alt_keys = ["level", "mouth", "state", "char_var", "extra1", "extra2"].
look (follow / avert) is not an alternative — both gaze files must exist.
extra1 (body) and extra2 (head) are optional special-sprite tokens; empty or
missing extras fall back to $. Event patterns pass alternative keys as extra
constructor args: Pattern("main", path, "level", "variant").
$ must appear in the filename on disk for a fallback to work. A missing
specific file with no $ sibling is a miss, not a silent skip of that token.
A path containing <nude> is a template, not a final file. Files are numbered:
images/events/foo/foo 1 0.webp # clothed
images/events/foo/foo 1 1.webp # nude 1
images/events/foo/foo 1 2.webp # nude 2
get_image reports the highest available nude index (capped by nude_vision) and
leaves <nude> in the returned template. show_ready_image then opens the
eye-icon UI so the player can step between versions.
In an event you usually do not handle this yourself: image.show(step) detects
<nude> and routes to that UI. For a one-off path:
$ nude, path = get_image("images/events/foo/foo <step> <nude>.webp", step=1)
# nude == 2, path still contains <nude>, extension already matches the files
call show_image_with_nude_var(path, nude)This is the common author path. Full event wiring is in Events §9; this section is which image helper to pick.
Pattern("main", "images/events/…/<topic> <step>.webp")
Pattern("card", "images/events/…/card <girls> <step>.webp", "girls") # girls may fall back to $Several named patterns per event ("main", "bg", "card", …). They are attached
to the event and show up in the label as kwargs["image_patterns"].
$ image = convert_pattern("main", **kwargs)
$ image.show(0) # SCENE, random variant if the pattern has <variant>
$ image.show(1)
$ image.show(2, SHOW) # overlay instead of replacing the scene$ image.show(n) (Image_Series.show) also paperdoll_manager.clear()s when a
manager exists — paperdolls and their backdrop go away with the still. No extra
clear_display(). (call show_image / show_pattern do not do this.)
convert_pattern("card", {"girls": "emiko"}, **kwargs) builds a series with one key
forced — useful when a second pattern must show a specific character. The optional
data dict is injected into the values layer, so it never collides with an existing
kwargs key (unlike a top-level named parameter, which raises on a duplicate and which
the resolver ignores once a values layer exists). For the inline named-argument form
use with_values: convert_pattern("card", **with_values(kwargs, girls="emiko")).
call Image_Series.show_image(image, 0, 1, 2) plays several steps on click with no
dialogue between them.
image[step] (getitem) returns a concrete loadable path or None — handy as a
paperdoll background: paperdoll_manager.set_background(image[2], blur=True).
$ show_pattern("main", **kwargs)Or, without a named pattern:
call show_image("images/background/school building/<level> 0 1.webp")show_image runs refine_image then show_ready_image. Prefer convert_pattern /
show_pattern when the path lives on the event so mods can overwrite it
(§13).
$ image.show_black()
$ image.hide()Screens do not have event kwargs. You typically have a concrete path (maybe with
one or two placeholders) and need to put it in an add / imagebutton.
| Need | Call |
|---|---|
| "Does this file exist, png or webp?" |
find_loadable_image(path) → the real path, or ""
|
| Boolean only |
check_image(path) (same fallback; still prefer displaying the resolved path) |
Fill <state> / <level> / a few keys |
refine_image(path, state="idle", level=3) |
Fill keys and $ fallbacks |
refine_image_with_alternatives(path, ["level"], level=3) then find_available_images(...)
|
# building sprite on the map — the Building already stored a prefixed pattern
$ idle = find_loadable_image(building.get_image("idle"))
if idle:
add idle:
xpos building.x_pos ypos building.y_pos
# journal thumbnail that may still contain <level>
$ thumb = find_loadable_image(refine_image(obj.thumbnail, **kwargs))
if thumb == "":
$ thumb = "images/journal/empty_image.webp"
add thumbDo not add path after a successful renpy.loadable(path) check unless path
is already the resolved file. find_loadable_image is the check and the path
to display.
When you store an image on a class, capture the mod prefix in the constructor
(while set_current_mod is set) and resolve at use time.
def __init__(self, image: str, ...):
self.image = get_mod_path(active_mod_key) + image if image else image
def get_image(self, state="idle"):
return refine_image(self.image, state=state)
# refine_image already swaps png/webp on the filled pathThat is what Building, Pictogram, ItemData, teasers and situation thumbnails
do. Callers that put the result on screen should still run find_loadable_image if
they need a miss → fallback image.
| System | Store | Resolve / show |
|---|---|---|
| Event |
Pattern("main", "images/…") on the Event
|
convert_pattern / show_pattern in the label |
| Paperdoll |
Person.register_paperdoll() (patterns include basePath) |
PDAImage + .display() — Paperdoll
|
| Person portraits |
basePath + images/characters/<name>/level_N.webp
|
person.get_portraits() / get_thumbnail() already resolve png/webp |
| Building map sprite | Building(..., "images/map/foo_<state>.webp") |
building.get_image("idle") then find_loadable_image in the screen |
| Map background | Map(key, "images/…", …) |
get_current_map().get_map_path() in map_overview / the map screens — Maps
|
| Situation thumbnail / teaser photo | constructor path |
situation.get_current_thumbnail() / teaser _resolve_image (uses get_image) |
| Pictogram icon | constructor path |
pictogram.get_icon(**kwargs) → refine_image
|
| Inventory item | constructor path |
item.get_image() → find_loadable_image
|
| Idle location BG |
BGStorage + BGImages |
call show_idle_image(bg_images) |
Character paperdolls are two layers whose patterns end in .png. Resolution is the
same pipeline (refine_image_with_alternatives → find_available_images). See
Paperdoll for registration, actions and backgrounds. A paperdoll
background accepts a pattern, a concrete path, or image[step] from an
Image_Series.
Location idle art uses BGImage / BGStorage, not event Patterns.
BGStorage(
"images/background/school building/1 0 1.webp", # fallback
BGImage("images/background/school building/<level> 0 1.webp", 1,
TimeCondition(daytime="d")),
)BGStorage stamps the current mod prefix onto each BGImage. call show_idle_image(storage)
picks the highest-priority image whose conditions pass, then shows it (with nude
toggle if the path has <nude>).
For a one-off backdrop behind a paperdoll, prefer
paperdoll_manager.set_background(...) rather than BGStorage.
A mod can replace one named pattern on an existing event without forking the label:
init 1 python:
set_current_mod('mymod_key')
overwrite_event_image(
"snack_chat",
"main",
Pattern("main", "images/snack_chat_alt/<topic> <step>.webp"))The new Pattern is prefixed into the mod folder. The scene label keeps calling
convert_pattern("main", **kwargs) and now loads your files. No-ops while the mod
is disabled.
A video is the animated twin of a stepped .webp frame. The Movie image name
must be anim_ + the resolved webp basename, spaces → underscores. Details and the
image.show_video(step, pause) call live in Events §10.
show_video derives that name from the resolved image path after png/webp
resolution, using the stem (so foo 1 0.webp and foo 1 0.png yield the same
anim_foo_1_0).
-
Write
images/…, nevermods/MyMod/…. Prefixing is the constructor's job. -
set_current_modbefore you construct anything that stores a path. - One extension in the pattern is enough. Ship png, webp, or a mix; do not duplicate patterns per format.
-
Prefer
Patternon the event over a hardcoded path in the label, so mods can overwrite it. -
$on disk for every alternative key you list, or that fallback will never hit. -
Resolve, then display.
find_loadable_image(path)(orrefine_image/convert_pattern) first;add/scenethe returned string. -
Do not call
renpy.loadableon pattern paths in new code. Usefind_loadable_image/check_image. -
<level>does not need a file per level. The resolver walks to the nearest existing file. -
Paperdoll patterns stay
.pngby convention; WebP assets still resolve. -
<school_level>art matches the climate. A level-1 still in a conservative uniform, a level-8 still in a nude hallway — see School Levels. If the event spans a wide level range, ship frames (or$fallbacks) that can carry the difference.
| Symptom | Likely cause | Fix |
|---|---|---|
Image missing, log shows .png
|
No png and no webp at that stem | Check folder, tokens, and $ fallbacks. |
Literal <key> in the log path |
Kwarg / selector never set | Add the selector or pass the key into refine_image / PDAImage. |
| Mod images not found |
set_current_mod missing, or you wrote mods/… yourself |
Set the context first; use plain images/…. |
| Wrong event art after adding a mod |
overwrite_event_image not used, or wrong event/pattern key |
Match the event key and pattern name ("main"). |
| Step 0 works, later steps empty | Gap in <step> numbering, or only png probed in old code |
Number steps densely from step_start; current probing sees both extensions. |
| Variant never appears | No 1…n files, or <variant> not in the pattern |
Add foo 0 1.webp, foo 0 2.webp, … |
| Nude toggle missing | Path has no <nude>, or only level 0 exists |
Name files … 0, … 1, … and keep <nude> in the pattern. |
Screen shows nothing / error on add
|
Displayed the pattern string after a png/webp miss |
path = find_loadable_image(...); add path only when non-empty. |
| Paperdoll layer blank | Specific file missing and no $ sibling for an alt_key
|
Add the file or a $ fallback; check pose/outfit are exact (not alt keys). |
Log path has a gap / double space before .png
|
An alt_key (extra1, extra2, state, …) was empty and used to interpolate as a blank token |
Empty alt-key values now become $ immediately. |
Pattern(key, path, *alternative_keys) · show_pattern(key, **kwargs) ·
convert_pattern(key, data=None, **kwargs) → Image_Series ·
with_values(kwargs, **values) (inject into the values layer, collision-free) ·
image.show(step, display_type=SCENE, variant=-1) ·
image.show_video(step, pause=False, variant=-1) ·
image[step] → concrete path · image.show_black() / image.hide() ·
call Image_Series.show_image(image, *steps) ·
overwrite_event_image(event_key, pattern_key, Pattern(...)).
refine_image(path, **kwargs) · refine_image_with_alternatives(path, alt_keys, **kwargs) ·
find_loadable_image(path) · find_available_images(paths) · check_image(path) ·
get_image(path, **kwargs) → (nude, path) ·
call show_image(path, display_type=SCENE, **kwargs) ·
get_mod_path(key) / get_current_mod_path().
apply_available_image_extension · image_extension_candidates ·
replace_image_extension · get_available_level · get_image_max_value /
get_image_max_value_with_alternatives · show_ready_image.
SCENE — replace the background (scene). SHOW — overlay as general_image.
-
Events — when to put a
Patternon an event, stepped scenes, videos -
Selectors — the values that fill
<key> - Paperdoll — layered sprites on top of this resolver
-
Modding —
set_current_mod, folder layout, asset redirection
-
game/scripts/images.rpy—Pattern,Image_Series, refine/loadable helpers, show labels -
game/scripts/helper.rpy—get_mod_path,set_current_mod -
game/scripts/paperdoll.rpy— layer/background resolution on top of this system -
game/scripts/character.rpy—PersonbasePath,register_paperdoll, portraits -
game/scripts/event.rpy— attachesPatterns asimage_patterns/frag_image_patterns
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