Dialogue - SuIT-pub/Mind-the-School GitHub Wiki
Audience: Developers writing Mind the School scenes who need someone to speak โ a line of dialogue, an inner thought, a whisper or a shout โ and want the name tag, colour, font and format to be right without hand-defining a Ren'Py
Characterper person.Scope: How a
Personbecomes a speaking voice (character.rpy), how the name tag is styled per role and per character, how the player-named Headmaster and Emiko fit in, and where names live. The on-screen sprite is a separate system โ see Paperdoll. Picking who speaks from a selector is in Events ยง11 and Selectors.
Grab the Person, then talk through its speech modes:
$ miwa = Person["miwa_igarashi"]
miwa "That was crazy..." # bare call == normal speech
miwa.say "That was crazy..." # same thing, explicit
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 membersThe player (Headmaster) and the secretary (Emiko) are ordinary Persons too, and
begin_event already binds them โ inside any event you can just speak:
$ begin_event(**kwargs) # binds `headmaster` and `emiko` for you
emiko "Caught you glaring at it. Don't worry โ everyone glares at it."
headmaster.think "That plaque again."
headmaster "Then let's stop waiting on it. Chase it today."The three moving parts:
-
Charโ a stat container (level + corruption/inhibition/โฆ). One per role/school. -
Personโ the actor: name, description, portraits, paperdoll, and the speaking voice. It points at aCharfor stats and for its styling category. -
The speaking
Characterโ built on demand byPerson.get_renpy_char(); you never define one per person unless you want to override its look.
- Char vs. Person
- Speaking as a fixed character
- Speaking as a selected character
- The Headmaster & Emiko
- Styling categories (the
kind) - Per-character overrides (
styleOverrides) - Names
- Registering a person
- Conventions
- Troubleshooting
- Reference tables
Two classes, two jobs โ don't conflate them.
-
Charholds the numbers: aleveland a bag ofStatobjects. The game runs on a small set of sharedChars โ one per role (school,parent,teacher,secretary), reached withget_character_by_key("..."). Every student shares the oneschoolchar; every teacher shares theteacherchar. That's why raising "the school's" inhibition moves the whole class at once. -
Personis the individual:name(the id),first_name/last_name, adescription,portraits, paperdoll data, andcharacterโ theCharit draws stats and styling from. APersonis what speaks.
So many Persons can share one Char. The Headmaster is the exception: he is the
player, has no stat-Char (character is None), and is handled by name โ see
ยง4.
Personobjects live inperson_storage, which is per-save, butload_personre-applies the code definition on every load. Treat aPersondefinition as code-authoritative and save-agnostic: name, description and styling come from the code each session; only genuinely per-save data (the Headmaster's chosen name, theChar's stats) lives in the save. Never bake per-save state into aPersondef.
Keep the Person and speak through it โ you get dialogue and the rest of the
person from one variable, no second definition:
$ miwa = Person["miwa_igarashi"]
miwa "Normal line." # __call__ delegates to .say
miwa.say "Normal line."
miwa.think "Inner monologue."
miwa.whisper "Kept quiet."
miwa.shout "Loud!"Person["key"] returns the Person (via __class_getitem__ โ find_person). Each
speech-mode property returns a fresh Ren'Py Character, so a say statement can use
it directly โ Ren'Py evaluates the who-expression, and dotted/subscript expressions are
allowed, so even Person["miwa_igarashi"].say "..." works inline. The bare
person "..." form works because Person.__call__ forwards to .say.
| Property / form | Mode | What it adds on top of the category look |
|---|---|---|
person "..." / .say
|
normal speech | nothing |
.think |
inner monologue | italic, prefix ( , suffix ), name suffix "(thinking)" |
.whisper |
whisper | italic, name suffix "(whispering)" |
.shout |
shout | bold, name suffix "(shouting)" |
Under the hood every one calls get_renpy_char(char_type="") (char_type โ "" /
"thought" / "whisper" / "shout"). The old
$ x = Person["key"].get_renpy_char(...) form still works and returns the same
Character; the property/bare-call form just avoids carrying that extra line. Building a
new Character per access is cheap and behaves identically (same name, retain=False).
When a selector picked who speaks (its value is a character key like
"aona_komuro" or a role like "school"), resolve it in the scene label:
$ girl = get_person_value("girl_name", **kwargs) # -> the Person
girl.say "Fine, I'll do the dare."
girl.think "...this is embarrassing."-
get_person_value(key, alt=None, **kwargs)โ the resolvedPerson(handles theschool/parent/teacher/secretaryroles and falls back to a neutral default view). Prefer this โ you get the speech-mode properties and the person's other members. -
get_person_char(key, alt=None, **kwargs)โ just a speakingCharacterfor the selected key. Use it when you only need a voice. -
get_person_char_with_key(group_key, name, char_type="")โ resolve a character from a storage group ("class_3a","staff","parents") plus a name.
This is why an event stays generic: the selector chooses who, and these helpers turn that choice into the voice that speaks.
Both are full Persons and speak through the same .say/.think/.whisper/.shout
API as everyone else. You don't bind them yourself: begin_event binds headmaster
and emiko at the start of every event (event.rpy), so inside the scene you can use
them straight away. (Outside an event โ e.g. a screen or a debug label that never calls
begin_event โ the static character.headmaster / character.emiko still catch a bare
say line; only the Person-object features like emiko.register_paperdoll() need a real
begin_event first.)
The Headmaster is special in two ways:
-
No stat-
Char. He's the player; hischaracterisNone. His styling is keyed byself.name == "headmaster"โ thecharacter.headmasterlook (see ยง5). -
A save-specific, player-chosen name. His
Personis defined name-less (Person("headmaster", "", "", ...)).get_first_name/get_last_name/get_full_namespecial-case him and read the live name fromget_name("headmaster")(see ยง7). This keeps the name per-save while thePersondefinition stays save-agnostic โ the name is never baked into the def.
Emiko is a normal Person (emiko_langley) whose Char is the secretary role, so
she automatically picks up the character.secretary look. Write her lines as emiko
(the old secretary "..." speaker was retired in favour of the Person).
Static safety nets
character.headmasterandcharacter.emikoexist invalues.rpyso a bareheadmaster/emikosay line can't crash even if a code path forgot the$ ... = Person[...]binding. The legacycharacter.*_thought/_whisper/_shoutdefinitions still exist (referenced by version-compat fixups) but are redundant for dialogue โ the char-type modes generate that formatting dynamically. Don't add new ones; use.think/.whisper/.shout.
Every speaking Character starts from a category โ a shared Ren'Py Character
defined in values.rpy that carries the name-tag colour, text colour and size for that
kind of speaker. get_renpy_char picks it:
Test on the Person
|
Category (kind) |
Name-tag colour |
|---|---|---|
name == "headmaster" |
character.headmaster |
white |
character is the school char |
character.sgirl |
purple |
character is the parent char |
character.parent |
lavender |
character is the teacher char |
character.teacher |
teal |
character is the secretary char |
character.secretary |
magenta |
| (none of the above) | character.subtitles |
neutral / centered |
The category sets the base; the char-type mode (.think/.whisper/.shout) layers
its markers on top; a per-character override (next section) can override anything in
between.
A Person can override any part of its speaking look without touching the shared
category. Pass styleOverrides โ a dict of Ren'Py Character kwargs โ to the
Person definition:
$ load_person("class_3a", Person("zoe_parker", "Zoe", "Parker", school_char, [ ... ],
styleOverrides = {
"who_color": "#e67e22", # name-tag colour
"what_color": "#fbeee0", # spoken-text colour
"what_font": "gui/font/handwritten.ttf",
"what_size": 30,
"what_italic": True,
}))Layering (last wins): category kind โ this Person's styleOverrides โ the
char-type markers. So an override changes the base for all of this person's modes,
while .whisper/.shout/.think still apply their own italic/bold/suffix on top.
Anything the override doesn't set falls through to the category.
character.sgirl (base) who_color #8a2be2, what_color #fff, what_size 28
โ styleOverrides who_color #e67e22, what_size 30
โ .whisper + what_italic, + name suffix "(whispering)"
= final Character who_color #e67e22, what_color #fff, what_size 30, italic, "(whispering)"
Common keys: who_color, what_color, who_font, what_font, who_size,
what_size, who_bold, what_bold, who_italic, what_italic, what_prefix,
what_suffix, window_background. (Any Character kwarg is accepted.)
styleOverrides is code-level โ copied from the definition on every load, like the
rest of the Person. An empty/omitted dict means "use the category as-is". Old saves
without the field are backfilled to empty.
A character's display name has two independent sources depending on whether it's player-editable.
Baked-in (most characters). first_name/last_name are set in the Person
definition and shown via get_first_name() / get_last_name() / get_full_name().
These are code-level, save-agnostic.
Editable (the Headmaster). His name is player-chosen and lives in the save:
-
set_name("headmaster", first, last)writesgameData["names"]["headmaster"]. -
get_name("headmaster")โ(first, last), falling back todefault_namesif unset. -
get_name_str("headmaster")โ a joined string. -
default_names(invalues.rpy) holds the defaults for every editable key.
The Headmaster Person's name getters read get_name("headmaster") live, so renaming
mid-game is reflected everywhere immediately.
Interpolating a name into another line. Other characters refer to the Headmaster with Ren'Py text tags, mirrored into per-save store variables:
sgirl "Hello Mr. [headmaster_last_name]."
emiko "Good morning, [headmaster_first_name]."headmaster_first_name / headmaster_last_name are kept in sync with
get_name("headmaster") on load. Use the tags in dialogue text; use the Person
getters in Python.
Persons are created in the load_characters label with load_person(group_key, Person(...)):
$ load_person("class_3a", Person(
"aona_komuro", # name / id (also the image folder)
"Aona", "Komuro", # first, last ("" , "" for the Headmaster)
school_char, # the Char this Person draws stats & category from
[ "โข Height: 172.5 cm", ... ], # description lines (see Characters page format)
styleOverrides = { ... }, # optional, see ยง6
))Storage groups: "class_3a" (students), "staff" (teachers, Emiko, Headmaster lives
under "NoView"), "parents", and "NoView" (default/neutral views not shown in the
journal). load_person is gated on the active mod, so a disabled mod's persons aren't
registered; base loaders run under set_current_mod("base").
Related: registering a sprite for the same person is a separate call โ
Person.register_paperdoll() / Person.display(), documented in Paperdoll.
-
headmasterandemikoare auto-bound bybegin_eventโ don't bind them yourself; just speak. For any other fixed character, bind it once near the top:$ x = Person["key"], then speak. -
Speak through the
Person, not a hand-definedCharacter. Reservevalues.rpyCharacterdefinitions for the shared categories and the two static safety nets (headmaster,emiko). -
Never bake per-save data into a
Persondef. The Headmaster's name proves the pattern: the def is name-less, the name is resolved live from the save. -
Style per character with
styleOverrides, not a new category, unless the whole role should change. - Use
[headmaster_first_name]/[headmaster_last_name]tags in dialogue text; useget_name(...)/ thePersongetters in Python. - Retire old speaker aliases: write
emiko,headmaster.thinkโ notsecretary "..."orheadmaster_thought "...".
| Symptom | Likely cause | Fix |
|---|---|---|
NameError: emiko is not defined on a say line |
the label never called begin_event (which binds headmaster/emiko) |
call $ begin_event(**kwargs) first, or for a non-event context rely on the static character.emiko
|
AttributeError: 'ADVCharacter' has no attribute 'register_paperdoll' |
using emiko.register_paperdoll() where emiko fell back to the static Character |
that call needs the Person โ make sure begin_event ran in this label |
Headmaster's name shows literal [headmaster_first_name]
|
reading the raw def field instead of a getter/tag | use get_full_name() in Python or the [...] tag in dialogue text |
| A character speaks with the neutral/centered subtitle look | their Person.character matches no known role (kind fell through to subtitles) |
check the Char passed to the Person; for the Headmaster the name must be exactly "headmaster"
|
styleOverrides seems ignored |
key typo, or a char-type marker overrides it (e.g. .whisper forces italic) |
verify the kwarg name; remember char-type markers win on their own keys |
AttributeError: ... has no attribute 'get_character' |
calling the old method name on a Person
|
the method is get_renpy_char(char_type="")
|
Lint prints Could not evaluate '<name>' in the who part
|
expected โ lint can't run the per-event $ x = Person[...] binding |
harmless; it's the same for every dynamically-bound speaker (aona, miwa, โฆ) |
Speech modes (get_renpy_char(char_type))
char_type |
Property | Formatting layered on the category |
|---|---|---|
"" |
.say / person "..."
|
โ |
"thought" |
.think |
italic, ( โฆ ), "(thinking)" |
"whisper" |
.whisper |
italic, "(whispering)" |
"shout" |
.shout |
bold, "(shouting)" |
Person API (dialogue-relevant)
| Member | Returns | Notes |
|---|---|---|
Person["key"] |
Person |
lookup across all storage groups |
person(...) / person.say
|
Character |
normal speech |
person.think / .whisper / .shout
|
Character |
speech modes |
person.get_renpy_char(char_type="") |
Character |
the underlying builder |
person.get_first_name() / get_last_name() / get_full_name()
|
str |
Headmaster reads get_name live |
Helpers
| Function | Returns | Where |
|---|---|---|
get_person(group_key, name) |
Person |
character.rpy |
find_person(name) |
Person |
character.rpy |
get_person_value(key, alt=None, **kwargs) |
Person |
gallery.rpy (selector value) |
get_person_char(key, alt=None, **kwargs) |
Character |
gallery.rpy |
get_person_char_with_key(group_key, name, char_type="") |
Character |
character.rpy |
get_character_by_key(key) |
Char |
character.rpy (role stat container) |
get_name(key) / set_name(key, first, last) / get_name_str(key)
|
name | helper.rpy |
Related files
-
game/scripts/character.rpyโChar,Person,get_renpy_char,styleOverrides,load_person,load_characters. -
game/scripts/values.rpyโ categoryCharacters, static safety nets,default_names. -
game/scripts/helper.rpyโset_name/get_name/get_name_str. -
game/scripts/gallery.rpyโget_person_value/get_person_char.
See also: Events ยง11 Characters & dialogue ยท Selectors ยท Paperdoll