use crate::interactive_fiction::data::{
DialogueId, Effect, EndingId, EntityLocation, FlagKey, ItemId, ItemLocation, NodeId,
Placeholder, QuestId, RoomId, RuleId, ScheduledEvent, StatKey, TimerId, TranscriptEntry, Value,
VerbResponses,
};
use crate::interactive_fiction::engine::{Engine, dispatch, eval, resolve};
use std::collections::VecDeque;
pub struct Context {
queue: VecDeque<dispatch::Event>,
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
pub fn new() -> Self {
Self {
queue: VecDeque::new(),
}
}
pub fn queue(&mut self, event: dispatch::Event) {
self.queue.push_back(event);
}
pub fn drain(
&mut self,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
) {
while let Some(event) = self.queue.pop_front() {
self.dispatch_event(engine, state, event);
if state.game_over.is_some() {
return;
}
}
}
fn dispatch_event(
&mut self,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
event: dispatch::Event,
) {
let rules = dispatch::candidates(engine, state, &event);
for rule_id in rules {
self.fire_rule(engine, state, rule_id);
if state.game_over.is_some() {
return;
}
}
}
fn fire_rule(
&mut self,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
rule_id: RuleId,
) {
if engine.rule_tracing_enabled() {
let line = VerbResponses::render(
&engine.world().verb_responses.trace_prefix,
&[(Placeholder::Rule, rule_id.as_str())],
);
state.push_transcript(TranscriptEntry::System(line));
}
let effects = engine
.world()
.rules
.get(&rule_id)
.map(|rule| rule.effects.clone())
.unwrap_or_default();
state.rules_fired.insert(rule_id.clone());
state.rule_last_fired.insert(rule_id, state.turn);
self.run_effects(engine, state, &effects);
}
pub fn run_effects(
&mut self,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
effects: &[Effect],
) {
for effect in effects {
if state.game_over.is_some() {
return;
}
self.run_effect(engine, state, effect);
}
}
fn run_effect(
&mut self,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
effect: &Effect,
) {
match effect {
Effect::Say(text) => {
let rendered = resolve::resolve_mut(engine, state, text);
if !rendered.is_empty() {
state.push_transcript(TranscriptEntry::Narration(rendered));
}
}
Effect::DescribeRoom => describe_current_room(engine, state),
Effect::ClearTranscript => state.transcript.clear(),
Effect::SetFlag(key, value) => set_flag(self, state, key.clone(), value.clone()),
Effect::UnsetFlag(key) => unset_flag(self, state, key.clone()),
Effect::AddStat(key, delta) => add_stat(state, key.clone(), *delta),
Effect::SetStat(key, value) => {
state.stats.insert(key.clone(), *value);
}
Effect::MoveItem(item, location) => {
move_item(state, item.clone(), location.clone());
}
Effect::MovePlayer(room) => move_player(self, engine, state, room.clone()),
Effect::MoveEntity(entity, location) => {
state
.entity_locations
.insert(entity.clone(), location.clone());
}
Effect::AdjustDisposition(entity, delta) => {
let current = state.dispositions.get(entity).copied().unwrap_or(0);
state.dispositions.insert(entity.clone(), current + *delta);
}
Effect::SetQuestStage(quest, stage) => {
set_quest_stage(self, engine, state, quest.clone(), stage.clone());
}
Effect::BeginDialogue(dialogue) => {
begin_dialogue(self, engine, state, dialogue.clone());
}
Effect::EndDialogue => {
state.active_dialogue = None;
}
Effect::GotoDialogue(node) => {
goto_dialogue(self, engine, state, node.clone());
}
Effect::If {
when,
then,
otherwise,
} => {
let chosen = if eval::evaluate_mut(engine, state, when) {
then
} else {
otherwise
};
self.run_effects(engine, state, chosen);
}
Effect::Sequence(inner) => self.run_effects(engine, state, inner),
Effect::OneOf(branches) => {
if !branches.is_empty() {
let pick = state.random_index(branches.len());
let owned = branches[pick].clone();
self.run_effects(engine, state, &owned);
}
}
Effect::OfferChoices(choices) => {
state.pending_choices = choices.clone();
}
Effect::TriggerEvent(name) => {
self.queue(dispatch::Event::Named(name.clone()));
}
Effect::FireRule(rule) => self.fire_rule(engine, state, rule.clone()),
Effect::StartTimer(timer) => start_timer(state, engine, timer.clone()),
Effect::CancelTimer(timer) => cancel_timer(state, timer.clone()),
Effect::ScheduleEvent { event, in_turns } => {
state.scheduled_events.push(ScheduledEvent {
event: event.clone(),
fires_on_turn: state.turn + *in_turns,
});
}
Effect::TriggerEnding(ending) => {
trigger_ending(self, engine, state, ending.clone());
}
}
}
}
pub(crate) fn describe_current_room(
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
) {
let Some(room) = engine.world().rooms.get(&state.current_room) else {
return;
};
let dark =
room.dark && !crate::interactive_fiction::engine::helpers::player_has_light(engine, state);
let header = VerbResponses::render(
&engine.world().verb_responses.room_header,
&[(Placeholder::Name, &room.name)],
);
state.push_transcript(TranscriptEntry::Narration(header));
let body = if dark {
room.dark_description.clone().unwrap_or_default()
} else {
room.description.clone()
};
let rendered = resolve::resolve_mut(engine, state, &body);
if !rendered.is_empty() {
state.push_transcript(TranscriptEntry::Narration(rendered));
}
if !dark {
describe_visible_here(engine, state);
}
describe_exits_here(engine, state, room);
}
fn describe_exits_here(
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
room: &crate::interactive_fiction::data::Room,
) {
if room.exits.is_empty() {
return;
}
let mut entries: Vec<String> = Vec::new();
for exit in &room.exits {
if let Some(visibility) = &exit.visible_when
&& !crate::interactive_fiction::engine::eval::evaluate(engine, state, visibility)
{
continue;
}
let head_slice = exit
.direction
.split(|c: char| c == '(' || c.is_whitespace())
.next()
.unwrap_or(exit.direction.as_str())
.trim();
let head = if head_slice.is_empty() {
exit.direction.as_str()
} else {
head_slice
};
let destination_label = engine.world().rooms.get(&exit.to).and_then(|target| {
if target.unseen_alias.is_none() {
return Some(target.name.clone());
}
let force_alias = match &target.alias_when {
Some(condition) => {
crate::interactive_fiction::engine::eval::evaluate(engine, state, condition)
}
None => !state.visited.contains(&exit.to),
};
if force_alias {
target.unseen_alias.clone()
} else {
Some(target.name.clone())
}
});
let entry = match destination_label {
Some(label) if !label.is_empty() => format!("{head} ({label})"),
_ => head.to_string(),
};
entries.push(entry);
}
let responses = &engine.world().verb_responses;
let line = if entries.is_empty() {
responses.exits_none.clone()
} else {
format!("{}{}.", responses.exits_listing_prefix, entries.join(", "))
};
state.push_transcript(TranscriptEntry::Narration(line));
}
fn describe_visible_here(
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
) {
let here = state.current_room.clone();
let mut visible: Vec<String> = Vec::new();
for (entity_id, location) in &state.entity_locations {
if matches!(location, EntityLocation::Room(room) if room == &here)
&& let Some(entity) = engine.world().entities.get(entity_id)
{
visible.push(entity.name.clone());
}
}
for (item_id, item) in &engine.world().items {
if matches!(
state.item_locations.get(item_id),
Some(ItemLocation::Room(room)) if room == &here
) {
visible.push(item.name.clone());
}
}
if visible.is_empty() {
return;
}
let prefix = &engine.world().verb_responses.visible_listing_prefix;
let line = format!("{prefix}{}.", visible.join(", "));
state.push_transcript(TranscriptEntry::Narration(line));
}
fn set_flag(
ctx: &mut Context,
state: &mut crate::interactive_fiction::data::RuntimeState,
key: FlagKey,
value: Value,
) {
let was_unset = matches!(state.flags.get(&key), Some(Value::Bool(false)) | None);
state.flags.insert(key.clone(), value);
if was_unset {
ctx.queue(dispatch::Event::FlagSet(key));
}
}
fn unset_flag(
ctx: &mut Context,
state: &mut crate::interactive_fiction::data::RuntimeState,
key: FlagKey,
) {
let was_set = matches!(state.flags.get(&key), Some(v) if !matches!(v, Value::Bool(false)));
state.flags.remove(&key);
if was_set {
ctx.queue(dispatch::Event::FlagUnset(key));
}
}
fn add_stat(state: &mut crate::interactive_fiction::data::RuntimeState, key: StatKey, delta: i64) {
let current = state.stats.get(&key).copied().unwrap_or(0);
state.stats.insert(key, current + delta);
}
fn move_item(
state: &mut crate::interactive_fiction::data::RuntimeState,
item: ItemId,
location: ItemLocation,
) {
state.item_locations.insert(item, location);
}
pub(crate) fn move_player(
ctx: &mut Context,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
room: RoomId,
) {
let previous = state.current_room.clone();
if previous != room {
ctx.queue(dispatch::Event::PlayerExited(previous.clone()));
}
state.previous_room = Some(previous.clone());
state.current_room = room.clone();
state.visited.insert(room.clone());
if previous != room {
ctx.queue(dispatch::Event::PlayerEntered(room.clone()));
}
state.push_transcript(TranscriptEntry::Separator);
describe_current_room(engine, state);
}
fn set_quest_stage(
ctx: &mut Context,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
quest: QuestId,
stage: NodeId,
) {
state.quest_stages.insert(quest.clone(), stage.clone());
state
.quest_history
.entry(quest.clone())
.or_default()
.insert(stage.clone());
if let Some(quest_def) = engine.world().quests.get(&quest)
&& let Some(stage_def) = quest_def.stages.get(&stage)
{
let on_enter = stage_def.on_enter.clone();
ctx.run_effects(engine, state, &on_enter);
}
}
fn begin_dialogue(
ctx: &mut Context,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
dialogue: DialogueId,
) {
let Some(def) = engine.world().dialogues.get(&dialogue) else {
return;
};
let start = def.start.clone();
state.active_dialogue = Some((dialogue.clone(), start.clone()));
run_dialogue_on_enter(ctx, engine, state, &dialogue, &start);
}
fn goto_dialogue(
ctx: &mut Context,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
node: NodeId,
) {
let Some((dialogue_id, _)) = state.active_dialogue.clone() else {
return;
};
state.active_dialogue = Some((dialogue_id.clone(), node.clone()));
run_dialogue_on_enter(ctx, engine, state, &dialogue_id, &node);
}
fn run_dialogue_on_enter(
ctx: &mut Context,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
dialogue: &DialogueId,
node: &NodeId,
) {
let (text, on_enter) = match engine
.world()
.dialogues
.get(dialogue)
.and_then(|d| d.nodes.get(node))
{
Some(def) => (def.text.clone(), def.on_enter.clone()),
None => return,
};
let speaker = engine
.world()
.dialogues
.get(dialogue)
.and_then(|_| {
engine.world().entities.values().find_map(|entity| {
if entity.dialogue.as_ref() == Some(dialogue) {
Some(entity.name.clone())
} else {
None
}
})
})
.unwrap_or_else(|| {
engine
.world()
.verb_responses
.dialogue_default_speaker
.clone()
});
let rendered = resolve::resolve_mut(engine, state, &text);
if !rendered.is_empty() {
state.push_transcript(TranscriptEntry::Dialogue {
speaker,
text: rendered,
});
}
ctx.run_effects(engine, state, &on_enter);
}
fn start_timer(
state: &mut crate::interactive_fiction::data::RuntimeState,
engine: &Engine,
timer: TimerId,
) {
if let Some(def) = engine.world().timers.get(&timer) {
state
.timers_remaining
.insert(timer.clone(), def.initial_turns);
state.timers_expired.remove(&timer);
state.timers_cancelled.remove(&timer);
}
}
fn cancel_timer(state: &mut crate::interactive_fiction::data::RuntimeState, timer: TimerId) {
if state.timers_remaining.remove(&timer).is_some() {
state.timers_cancelled.insert(timer);
}
}
fn trigger_ending(
ctx: &mut Context,
engine: &Engine,
state: &mut crate::interactive_fiction::data::RuntimeState,
ending: EndingId,
) {
state.unlocked_endings.insert(ending.clone());
state.game_over = Some(ending.clone());
if let Some(def) = engine.world().endings.get(&ending) {
let header = format!("*** {} ***", def.title);
state.push_transcript(TranscriptEntry::Separator);
state.push_transcript(TranscriptEntry::Narration(header));
let description = def.description.clone();
let epilogue = def.epilogue.clone();
let rendered_desc = resolve::resolve_mut(engine, state, &description);
if !rendered_desc.is_empty() {
state.push_transcript(TranscriptEntry::Narration(rendered_desc));
}
let rendered_epilogue = resolve::resolve_mut(engine, state, &epilogue);
if !rendered_epilogue.is_empty() {
state.push_transcript(TranscriptEntry::Narration(rendered_epilogue));
}
}
state.pending_choices.clear();
let _ = ctx;
}