use crate::interactive_fiction::data::{
Choice, ChoiceAction, Entity, EntityId, EntityKind, EntityLocation, ExamineTarget,
ItemLocation, Placeholder, Room, RuntimeState, Text, VerbResponses,
};
use crate::interactive_fiction::engine::{Engine, eval};
pub fn assemble(engine: &Engine, state: &RuntimeState) -> Vec<Choice> {
if state.game_over.is_some() {
return Vec::new();
}
if !state.pending_choices.is_empty() {
let filtered = filter_selectable(engine, state, &state.pending_choices);
if !filtered.is_empty() {
return filtered;
}
}
if state.active_dialogue.is_some() {
return dialogue_choices(engine, state);
}
let mut choices = Vec::new();
let Some(room) = engine.world().rooms.get(&state.current_room) else {
return choices;
};
let dark =
room.dark && !crate::interactive_fiction::engine::helpers::player_has_light(engine, state);
let responses = &engine.world().verb_responses;
append_exits(engine, state, room, &mut choices);
if !dark {
append_room_items(engine, state, &mut choices);
append_entities(engine, state, &mut choices);
append_room_features(responses, room, &mut choices);
}
append_inventory_actions(engine, state, &mut choices);
append_utility(responses, &mut choices);
filter_selectable(engine, state, &choices)
}
fn append_exits(engine: &Engine, state: &RuntimeState, room: &Room, out: &mut Vec<Choice>) {
let responses = &engine.world().verb_responses;
for (index, exit) in room.exits.iter().enumerate() {
if let Some(visibility) = &exit.visible_when
&& !eval::evaluate(engine, state, visibility)
{
continue;
}
let label_text =
VerbResponses::render(&responses.choice_go, &[(Placeholder::Dir, &exit.direction)]);
let action = ChoiceAction::Go {
to: exit.to.clone(),
exit_index: index,
};
let mut choice = Choice::new(Text::lit(label_text), action);
if let Some(condition) = &exit.passable_when {
choice.condition = Some(condition.clone());
choice.visible_when_locked = true;
choice.locked_reason = exit
.locked_message
.clone()
.or_else(|| Some(Text::lit(responses.exit_locked_default.clone())));
}
out.push(choice);
}
}
fn append_room_items(engine: &Engine, state: &RuntimeState, out: &mut Vec<Choice>) {
let here = &state.current_room;
let responses = &engine.world().verb_responses;
for (item_id, item) in &engine.world().items {
let here_match = matches!(
state.item_locations.get(item_id),
Some(ItemLocation::Room(room)) if room == here
);
if !here_match {
continue;
}
if item.properties.takeable {
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_take,
&[(Placeholder::Item, &item.name)],
)),
ChoiceAction::Take(item_id.clone()),
));
}
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_examine_item,
&[(Placeholder::Item, &item.name)],
)),
ChoiceAction::Examine(ExamineTarget::Item(item_id.clone())),
));
}
}
fn append_inventory_actions(engine: &Engine, state: &RuntimeState, out: &mut Vec<Choice>) {
let responses = &engine.world().verb_responses;
for (item_id, item) in &engine.world().items {
if !matches!(
state.item_locations.get(item_id),
Some(ItemLocation::Inventory)
) {
continue;
}
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_use,
&[(Placeholder::Item, &item.name)],
)),
ChoiceAction::Use(item_id.clone()),
));
if item.properties.readable {
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_read,
&[(Placeholder::Item, &item.name)],
)),
ChoiceAction::Read(item_id.clone()),
));
}
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_examine_item,
&[(Placeholder::Item, &item.name)],
)),
ChoiceAction::Examine(ExamineTarget::Item(item_id.clone())),
));
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_drop,
&[(Placeholder::Item, &item.name)],
)),
ChoiceAction::Drop(item_id.clone()),
));
}
}
fn append_entities(engine: &Engine, state: &RuntimeState, out: &mut Vec<Choice>) {
let here = &state.current_room;
let responses = &engine.world().verb_responses;
let present: Vec<(EntityId, &Entity)> = state
.entity_locations
.iter()
.filter(|(_, location)| matches!(location, EntityLocation::Room(room) if room == here))
.filter_map(|(id, _)| {
engine
.world()
.entities
.get(id)
.map(|entity| (id.clone(), entity))
})
.collect();
for (entity_id, entity) in present {
if entity.dialogue.is_some() {
let template = match entity.kind {
EntityKind::Character { .. } => &responses.choice_talk,
EntityKind::Object => &responses.choice_open_object,
};
out.push(Choice::new(
Text::lit(VerbResponses::render(
template,
&[(Placeholder::Entity, &entity.name)],
)),
ChoiceAction::Open(entity_id.clone()),
));
}
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_examine_entity,
&[(Placeholder::Entity, &entity.name)],
)),
ChoiceAction::Examine(ExamineTarget::Entity(entity_id)),
));
}
}
fn append_room_features(responses: &VerbResponses, room: &Room, out: &mut Vec<Choice>) {
for keyword in room.examine.keys() {
out.push(Choice::new(
Text::lit(VerbResponses::render(
&responses.choice_examine_feature,
&[(Placeholder::Keyword, keyword)],
)),
ChoiceAction::Examine(ExamineTarget::Keyword(keyword.clone())),
));
}
}
fn append_utility(responses: &VerbResponses, out: &mut Vec<Choice>) {
out.push(Choice::new(
Text::lit(responses.choice_look.clone()),
ChoiceAction::Look,
));
out.push(Choice::new(
Text::lit(responses.choice_inventory.clone()),
ChoiceAction::Inventory,
));
out.push(Choice::new(
Text::lit(responses.choice_wait.clone()),
ChoiceAction::Wait,
));
}
fn dialogue_choices(engine: &Engine, state: &RuntimeState) -> Vec<Choice> {
let Some((dialogue_id, node_id)) = &state.active_dialogue else {
return Vec::new();
};
let Some(dialogue) = engine.world().dialogues.get(dialogue_id) else {
return Vec::new();
};
let Some(node) = dialogue.nodes.get(node_id) else {
return Vec::new();
};
let mut choices = Vec::new();
for (index, option) in node.options.iter().enumerate() {
let action = ChoiceAction::DialogueOption(index);
let mut choice = Choice::new(option.label.clone(), action);
if let Some(condition) = &option.condition {
choice.condition = Some(condition.clone());
if option.visible_when_locked {
choice.visible_when_locked = true;
choice.locked_reason = option.locked_reason.clone();
}
}
choices.push(choice);
}
if choices.is_empty() {
choices.push(Choice::new(
Text::lit(engine.world().verb_responses.choice_leave_dialogue.clone()),
ChoiceAction::LeaveDialogue,
));
}
filter_selectable(engine, state, &choices)
}
fn filter_selectable(engine: &Engine, state: &RuntimeState, choices: &[Choice]) -> Vec<Choice> {
choices
.iter()
.filter(|choice| match &choice.condition {
None => true,
Some(condition) => {
eval::evaluate(engine, state, condition) || choice.visible_when_locked
}
})
.cloned()
.collect()
}
pub fn is_actionable(engine: &Engine, state: &RuntimeState, choice: &Choice) -> bool {
match &choice.condition {
None => true,
Some(condition) => eval::evaluate(engine, state, condition),
}
}