use crate::interactive_fiction::data::{
EntityId, EventName, ExamineTarget, FlagKey, ItemId, RoomId, Rule, RuleId, Trigger,
TriggerKind, World,
};
use crate::interactive_fiction::engine::{Engine, eval};
use std::collections::HashMap;
pub struct RuleIndex {
by_kind: HashMap<TriggerKind, Vec<RuleId>>,
}
impl RuleIndex {
pub fn build(world: &World) -> Self {
let mut by_kind: HashMap<TriggerKind, Vec<RuleId>> = HashMap::new();
for (id, rule) in &world.rules {
by_kind
.entry(rule.trigger.kind())
.or_default()
.push(id.clone());
}
for bucket in by_kind.values_mut() {
bucket.sort();
}
Self { by_kind }
}
pub fn rules_for(&self, kind: TriggerKind) -> &[RuleId] {
self.by_kind.get(&kind).map(Vec::as_slice).unwrap_or(&[])
}
}
#[derive(Debug, Clone)]
pub enum Event {
GameStart,
TurnStart,
TurnEnd,
PlayerEntered(RoomId),
PlayerExited(RoomId),
TakeItem(ItemId),
DropItem(ItemId),
UseItem {
item: ItemId,
room: RoomId,
},
Examine(ExamineTarget),
Open(EntityId),
FlagSet(FlagKey),
FlagUnset(FlagKey),
Named(EventName),
}
impl Event {
pub fn kind(&self) -> TriggerKind {
match self {
Event::GameStart => TriggerKind::GameStart,
Event::TurnStart => TriggerKind::TurnStart,
Event::TurnEnd => TriggerKind::TurnEnd,
Event::PlayerEntered(_) => TriggerKind::OnEnter,
Event::PlayerExited(_) => TriggerKind::OnExit,
Event::TakeItem(_) => TriggerKind::OnTake,
Event::DropItem(_) => TriggerKind::OnDrop,
Event::UseItem { .. } => TriggerKind::OnUse,
Event::Examine(_) => TriggerKind::OnExamine,
Event::Open(_) => TriggerKind::OnOpen,
Event::FlagSet(_) => TriggerKind::OnFlagSet,
Event::FlagUnset(_) => TriggerKind::OnFlagUnset,
Event::Named(_) => TriggerKind::Named,
}
}
}
fn trigger_matches(trigger: &Trigger, event: &Event) -> bool {
match trigger {
Trigger::GameStart => matches!(event, Event::GameStart),
Trigger::TurnStart => matches!(event, Event::TurnStart),
Trigger::TurnEnd => matches!(event, Event::TurnEnd),
Trigger::OnEnter(target) => match event {
Event::PlayerEntered(room) => target.as_ref().is_none_or(|id| id == room),
_ => false,
},
Trigger::OnExit(target) => match event {
Event::PlayerExited(room) => target.as_ref().is_none_or(|id| id == room),
_ => false,
},
Trigger::OnTake(target) => match event {
Event::TakeItem(item) => target.as_ref().is_none_or(|id| id == item),
_ => false,
},
Trigger::OnDrop(target) => match event {
Event::DropItem(item) => target.as_ref().is_none_or(|id| id == item),
_ => false,
},
Trigger::OnUse { item, in_room } => match event {
Event::UseItem {
item: ev_item,
room: ev_room,
} => {
item.as_ref().is_none_or(|id| id == ev_item)
&& in_room.as_ref().is_none_or(|id| id == ev_room)
}
_ => false,
},
Trigger::OnExamine(target) => match event {
Event::Examine(event_target) => target.as_ref().is_none_or(|want| want == event_target),
_ => false,
},
Trigger::OnOpen(target) => match event {
Event::Open(entity) => target.as_ref().is_none_or(|id| id == entity),
_ => false,
},
Trigger::OnFlagSet(key) => match event {
Event::FlagSet(ev_key) => key == ev_key,
_ => false,
},
Trigger::OnFlagUnset(key) => match event {
Event::FlagUnset(ev_key) => key == ev_key,
_ => false,
},
Trigger::Named(name) => match event {
Event::Named(ev_name) => name == ev_name,
_ => false,
},
}
}
pub fn candidates(
engine: &Engine,
state: &crate::interactive_fiction::data::RuntimeState,
event: &Event,
) -> Vec<RuleId> {
let mut chosen: Vec<(&Rule, RuleId)> = Vec::new();
for rule_id in engine.rule_index().rules_for(event.kind()) {
let Some(rule) = engine.world().rules.get(rule_id) else {
continue;
};
if !trigger_matches(&rule.trigger, event) {
continue;
}
if rule.once && state.rules_fired.contains(rule_id) {
continue;
}
if rule.cooldown_turns > 0
&& let Some(last) = state.rule_last_fired.get(rule_id)
&& state.turn.saturating_sub(*last) < rule.cooldown_turns
{
continue;
}
if !eval::evaluate(engine, state, &rule.condition) {
continue;
}
chosen.push((rule, rule_id.clone()));
}
chosen.sort_by(|a, b| b.0.priority.cmp(&a.0.priority).then_with(|| a.1.cmp(&b.1)));
chosen.into_iter().map(|(_, id)| id).collect()
}