1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
//! Every player-facing string the engine emits on its own initiative.
//!
//! The engine never hard-codes flavour text. When it needs to narrate a
//! take, a drop, a dialogue speaker fallback, or a choice-menu label, it
//! reads the relevant field from `World.verb_responses` and substitutes
//! any `{placeholder}` tokens via [`VerbResponses::render`].
//!
//! Authors can override any field when building a world; the defaults are
//! plain English and match what the engine used to hard-code. Tokens per
//! field are documented inline.
use super::verb::{RefusalCategory, Verb};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn default_exits_listing_prefix() -> String {
"Exits: ".to_string()
}
fn default_exits_none() -> String {
"No obvious exits.".to_string()
}
/// The full set of templates the engine draws on. Placeholders are
/// `{item}`, `{npc}`, `{dir}`, `{keyword}`, `{room}`, `{rule}`, depending
/// on the field. Fields without any placeholder are literal strings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerbResponses {
// ---- Choice-menu labels --------------------------------------------
/// Template: `{dir}`.
pub choice_go: String,
/// Template: `{item}`.
pub choice_take: String,
/// Template: `{item}`.
pub choice_examine_item: String,
/// Template: `{item}`.
pub choice_use: String,
/// Template: `{item}`.
pub choice_read: String,
/// Template: `{item}`.
pub choice_drop: String,
/// Template: `{entity}`. Used for characters (NPCs).
pub choice_talk: String,
/// Template: `{entity}`. Used for objects (fixtures).
pub choice_open_object: String,
/// Template: `{entity}`. Used for any entity — character or object.
pub choice_examine_entity: String,
/// Template: `{keyword}`.
pub choice_examine_feature: String,
pub choice_look: String,
pub choice_inventory: String,
pub choice_wait: String,
pub choice_leave_dialogue: String,
/// Fallback when a passable-when-locked exit has no `locked_message`.
pub exit_locked_default: String,
// ---- Room description ----------------------------------------------
/// Template: `{name}`.
pub room_header: String,
/// Prefix preceding a comma-joined list of visible entities.
/// Combined string shape: `"{visible_listing_prefix}X, Y, Z."`
pub visible_listing_prefix: String,
/// Prefix preceding a comma-joined list of available exit directions.
/// Combined string shape: `"{exits_listing_prefix}north, south, up."`
#[serde(default = "default_exits_listing_prefix")]
pub exits_listing_prefix: String,
/// Shown in place of the exits listing when the room has no visible
/// exits (or all exits are hidden by `visible_when` conditions).
#[serde(default = "default_exits_none")]
pub exits_none: String,
// ---- Verb responses ------------------------------------------------
pub inventory_empty: String,
/// Prefix preceding a comma-joined list of inventory items.
pub inventory_listing_prefix: String,
/// Template: `{item}`.
pub take_success: String,
/// Template: `{item}`.
pub take_already_carrying: String,
/// Template: `{item}`.
pub take_not_takeable: String,
/// Template: `{item}`.
pub drop_success: String,
/// Template: `{item}`.
pub use_not_carrying: String,
/// Template: `{item}`.
pub read_nothing_written: String,
pub examine_unknown: String,
/// Template: `{entity}`. Fires when the player opens an entity
/// (character or object) that has no dialogue attached.
pub npc_silent: String,
pub leave_dialogue: String,
pub wait: String,
/// Fallback dialogue speaker label if no NPC references the dialogue.
pub dialogue_default_speaker: String,
// ---- Meta ----------------------------------------------------------
pub option_unavailable: String,
pub action_forbidden: String,
/// Template: `{rule}`. Emitted only when rule tracing is enabled.
pub trace_prefix: String,
// ---- Verb refusals -------------------------------------------------
//
// When the parser recognises a verb but the current menu has no
// target for it (and no examine fallback matched), it emits one of
// these lines instead of a flat "You can't do that here." Each
// template may reference `{noun}` (the raw token the player typed).
/// Catch-all — used for verbs not in any of the categories below.
pub refusal_default: String,
/// `taste {noun}`, `lick {noun}`.
pub refusal_taste: String,
/// `eat {noun}`, `drink {noun}`.
pub refusal_consume: String,
/// `attack`, `hit`, `kick`, `break`, `smash`, `cut`, `burn`, `punch`,
/// `strike`, `kill`, `fight`.
pub refusal_violence: String,
/// `kiss {noun}`, `apologise (to {noun})`.
pub refusal_affection: String,
/// `open`, `close`, `push`, `pull`, `turn`, `squeeze`, `rub`,
/// `wave`, `swing`, `wear`, `remove`, `insert`, `fill`, `lock`,
/// `unlock`, `switch`, `tie`. The item doesn't accept the action.
pub refusal_manipulation: String,
/// Bare `jump`, `jump {noun}`.
pub refusal_jump: String,
/// `give`, `show`, `buy`. Multi-object or commerce verbs we don't
/// model.
pub refusal_exchange: String,
/// `wake`, `rouse`.
pub refusal_wake: String,
/// Per-verb overrides for the refusal category. Any verb absent from
/// this map uses [`Verb::default_refusal_category`]. This is the
/// author-facing knob for re-routing — no parser edit needed to make
/// `Verb::Throw` refuse as violence instead of manipulation.
#[serde(default)]
pub refusal_overrides: HashMap<Verb, RefusalCategory>,
}
impl Default for VerbResponses {
fn default() -> Self {
Self {
choice_go: "Go {dir}".to_string(),
choice_take: "Take the {item}".to_string(),
choice_examine_item: "Examine the {item}".to_string(),
choice_use: "Use the {item}".to_string(),
choice_read: "Read the {item}".to_string(),
choice_drop: "Drop the {item}".to_string(),
choice_talk: "Talk to {entity}".to_string(),
choice_open_object: "Open {entity}".to_string(),
choice_examine_entity: "Examine {entity}".to_string(),
choice_examine_feature: "Look at the {keyword}".to_string(),
choice_look: "Look".to_string(),
choice_inventory: "Check inventory".to_string(),
choice_wait: "Wait".to_string(),
choice_leave_dialogue: "Leave the conversation".to_string(),
exit_locked_default: "locked.".to_string(),
room_header: "--- {name} ---".to_string(),
visible_listing_prefix: "You see: ".to_string(),
exits_listing_prefix: default_exits_listing_prefix(),
exits_none: default_exits_none(),
inventory_empty: "You are carrying nothing.".to_string(),
inventory_listing_prefix: "You are carrying: ".to_string(),
take_success: "You take the {item}.".to_string(),
take_already_carrying: "You are already carrying the {item}.".to_string(),
take_not_takeable: "The {item} won't move.".to_string(),
drop_success: "You drop the {item}.".to_string(),
use_not_carrying: "You aren't carrying the {item}.".to_string(),
read_nothing_written: "There is nothing to read on the {item}.".to_string(),
examine_unknown: "You see nothing special about that.".to_string(),
npc_silent: "{entity} has nothing to say.".to_string(),
leave_dialogue: "You step away from the conversation.".to_string(),
wait: "Time drifts past.".to_string(),
dialogue_default_speaker: "Voice".to_string(),
option_unavailable: "That option is not available.".to_string(),
action_forbidden: "You cannot do that.".to_string(),
trace_prefix: "[trace] rule '{rule}' fired".to_string(),
refusal_default: "Nothing happens.".to_string(),
refusal_taste: "That wouldn't taste of anything worth knowing.".to_string(),
refusal_consume: "That isn't food or drink.".to_string(),
refusal_violence: "Violence won't help you here.".to_string(),
refusal_affection: "That would be out of place.".to_string(),
refusal_manipulation: "It doesn't yield.".to_string(),
refusal_jump: "You hop on the spot. Nothing changes.".to_string(),
refusal_exchange: "There's no one here to accept that.".to_string(),
refusal_wake: "You are already awake.".to_string(),
refusal_overrides: HashMap::new(),
}
}
}
impl VerbResponses {
/// Substitute every occurrence of each [`Placeholder`] token in
/// `template` with the paired value. Missing placeholders are left
/// alone — useful if an author wants to embed literal braces.
pub fn render(template: &str, replacements: &[(Placeholder, &str)]) -> String {
let mut output = template.to_string();
for (placeholder, value) in replacements {
output = output.replace(placeholder.token(), value);
}
output
}
/// Resolve a verb's refusal category, consulting
/// [`Self::refusal_overrides`] first and falling back to
/// [`Verb::default_refusal_category`].
pub fn refusal_category(&self, verb: Verb) -> RefusalCategory {
self.refusal_overrides
.get(&verb)
.copied()
.unwrap_or_else(|| verb.default_refusal_category())
}
/// Convenience builder: route `verb` to `category` on refusal.
pub fn override_refusal(mut self, verb: Verb, category: RefusalCategory) -> Self {
self.refusal_overrides.insert(verb, category);
self
}
}
/// Every placeholder name a [`VerbResponses`] template may contain. Using
/// this enum at call sites guarantees we can't mistype a token like
/// `"itme"` or use one that the struct docs don't advertise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placeholder {
/// `{item}` — an item's display name.
Item,
/// `{entity}` — the display name of an entity (character or
/// object). Replaces the old `{npc}` and `{fixture}` tokens.
Entity,
/// `{dir}` — an exit's direction label.
Dir,
/// `{keyword}` — a room's examine-feature keyword.
Keyword,
/// `{name}` — a room's display name.
Name,
/// `{noun}` — the raw noun the player typed, after article/preposition
/// stripping. Used in verb-refusal templates.
Noun,
/// `{rule}` — a rule's ID.
Rule,
/// `{items}` — a comma-joined list of item names.
Items,
/// `{things}` — a comma-joined list of visible entities.
Things,
}
impl Placeholder {
/// The literal `{name}` token this placeholder replaces.
pub const fn token(self) -> &'static str {
match self {
Placeholder::Item => "{item}",
Placeholder::Entity => "{entity}",
Placeholder::Dir => "{dir}",
Placeholder::Keyword => "{keyword}",
Placeholder::Name => "{name}",
Placeholder::Noun => "{noun}",
Placeholder::Rule => "{rule}",
Placeholder::Items => "{items}",
Placeholder::Things => "{things}",
}
}
}