use crate::metatile::TriggerType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Trigger {
pub id: String,
pub map_id: String,
pub trigger_type: TriggerType,
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub script_name: String,
pub one_shot: bool,
pub fired: bool,
}
impl Trigger {
pub fn single_tile(
id: impl Into<String>,
map_id: impl Into<String>,
trigger_type: TriggerType,
x: u32,
y: u32,
script_name: impl Into<String>,
one_shot: bool,
) -> Self {
Self {
id: id.into(),
map_id: map_id.into(),
trigger_type,
x,
y,
width: 1,
height: 1,
script_name: script_name.into(),
one_shot,
fired: false,
}
}
#[inline]
pub fn contains(&self, tile_x: u32, tile_y: u32) -> bool {
tile_x >= self.x
&& tile_x < self.x.saturating_add(self.width)
&& tile_y >= self.y
&& tile_y < self.y.saturating_add(self.height)
}
}
#[derive(Debug, Clone, Default)]
pub struct TriggerManager {
triggers: Vec<Trigger>,
prev_player_map: Option<String>,
prev_player_x: u32,
prev_player_y: u32,
}
impl TriggerManager {
pub fn new() -> Self {
Self {
triggers: Vec::new(),
prev_player_map: None,
prev_player_x: 0,
prev_player_y: 0,
}
}
pub fn add_trigger(&mut self, trigger: Trigger) {
self.triggers.push(trigger);
}
pub fn add_triggers(&mut self, triggers: impl IntoIterator<Item = Trigger>) {
self.triggers.extend(triggers);
}
pub fn remove_triggers_for_map(&mut self, map_id: &str) {
self.triggers.retain(|t| t.map_id != map_id);
}
pub fn reset_fired_for_map(&mut self, map_id: &str) {
for trigger in &mut self.triggers {
if trigger.map_id == map_id {
trigger.fired = false;
}
}
}
pub fn all_triggers(&self) -> impl Iterator<Item = &Trigger> {
self.triggers.iter()
}
pub fn len(&self) -> usize {
self.triggers.len()
}
pub fn is_empty(&self) -> bool {
self.triggers.is_empty()
}
pub fn check_triggers(&mut self, map_id: &str, player_x: u32, player_y: u32) -> Vec<String> {
let same_map = self.prev_player_map.as_deref() == Some(map_id);
let was_inside = |t: &Trigger| -> bool {
same_map && t.contains(self.prev_player_x, self.prev_player_y)
};
let is_inside = |t: &Trigger| -> bool { t.contains(player_x, player_y) };
let mut triggered = Vec::new();
for trigger in &mut self.triggers {
if trigger.map_id != map_id {
continue;
}
if trigger.one_shot && trigger.fired {
continue;
}
let should_fire = match trigger.trigger_type {
TriggerType::OnStep => is_inside(trigger),
TriggerType::OnEnter => is_inside(trigger) && !was_inside(trigger),
TriggerType::OnInteract => false,
};
if should_fire {
trigger.fired = true;
triggered.push(trigger.script_name.clone());
}
}
self.prev_player_map = Some(map_id.to_string());
self.prev_player_x = player_x;
self.prev_player_y = player_y;
triggered
}
pub fn check_interact(
&self,
map_id: &str,
facing_x: u32,
facing_y: u32,
) -> Option<&str> {
self.triggers
.iter()
.find(|t| {
t.map_id == map_id
&& t.trigger_type == TriggerType::OnInteract
&& (!t.one_shot || !t.fired)
&& t.contains(facing_x, facing_y)
})
.map(|t| t.script_name.as_str())
}
pub fn check_interact_mut(
&mut self,
map_id: &str,
facing_x: u32,
facing_y: u32,
) -> Option<String> {
for trigger in &mut self.triggers {
if trigger.map_id != map_id {
continue;
}
if trigger.trigger_type != TriggerType::OnInteract {
continue;
}
if trigger.one_shot && trigger.fired {
continue;
}
if trigger.contains(facing_x, facing_y) {
trigger.fired = true;
return Some(trigger.script_name.clone());
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_trigger(
id: &str,
map_id: &str,
tt: TriggerType,
x: u32,
y: u32,
script: &str,
one_shot: bool,
) -> Trigger {
Trigger::single_tile(id, map_id, tt, x, y, script, one_shot)
}
#[test]
fn on_step_fires_when_standing() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnStep, 2, 3, "stepFn", false));
mgr.check_triggers("map1", 0, 0);
let names = mgr.check_triggers("map1", 2, 3);
assert_eq!(names, vec!["stepFn"]);
}
#[test]
fn on_step_fires_every_frame() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnStep, 2, 3, "stepFn", false));
mgr.check_triggers("map1", 0, 0); let first = mgr.check_triggers("map1", 2, 3);
let second = mgr.check_triggers("map1", 2, 3);
assert_eq!(first, vec!["stepFn"]);
assert_eq!(second, vec!["stepFn"]);
}
#[test]
fn on_step_does_not_fire_when_not_standing() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnStep, 2, 3, "stepFn", false));
mgr.check_triggers("map1", 0, 0);
let names = mgr.check_triggers("map1", 5, 5);
assert!(names.is_empty());
}
#[test]
fn on_enter_fires_once_on_entry() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnEnter, 2, 3, "enterFn", false));
mgr.check_triggers("map1", 0, 0);
let names = mgr.check_triggers("map1", 2, 3);
assert_eq!(names, vec!["enterFn"]);
}
#[test]
fn on_enter_does_not_refire() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnEnter, 2, 3, "enterFn", false));
mgr.check_triggers("map1", 0, 0); mgr.check_triggers("map1", 2, 3); let second = mgr.check_triggers("map1", 2, 3); assert!(second.is_empty());
}
#[test]
fn on_enter_fires_on_different_map_entry() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map2", TriggerType::OnEnter, 0, 0, "enterFn", false));
mgr.check_triggers("map1", 5, 5); let names = mgr.check_triggers("map2", 0, 0); assert_eq!(names, vec!["enterFn"]);
}
#[test]
fn one_shot_trigger_fires_once() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnStep, 2, 3, "onceFn", true));
mgr.check_triggers("map1", 0, 0);
let first = mgr.check_triggers("map1", 2, 3);
let second = mgr.check_triggers("map1", 2, 3);
assert_eq!(first, vec!["onceFn"]);
assert!(second.is_empty());
}
#[test]
fn one_shot_reset_allows_refire() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnStep, 2, 3, "onceFn", true));
mgr.check_triggers("map1", 0, 0);
mgr.check_triggers("map1", 2, 3); mgr.reset_fired_for_map("map1");
mgr.check_triggers("map1", 0, 0);
let again = mgr.check_triggers("map1", 2, 3);
assert_eq!(again, vec!["onceFn"]);
}
#[test]
fn on_interact_fires_on_facing_tile() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnInteract, 5, 5, "talkFn", false));
let name = mgr.check_interact("map1", 5, 5);
assert_eq!(name, Some("talkFn"));
}
#[test]
fn on_interact_ignores_wrong_position() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnInteract, 5, 5, "talkFn", false));
let name = mgr.check_interact("map1", 0, 0);
assert!(name.is_none());
}
#[test]
fn on_interact_mut_marks_fired() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnInteract, 5, 5, "talkFn", true));
let first = mgr.check_interact_mut("map1", 5, 5);
assert_eq!(first, Some("talkFn".into()));
let second = mgr.check_interact("map1", 5, 5);
assert!(second.is_none());
}
#[test]
fn area_trigger_2x2() {
let mut mgr = TriggerManager::new();
let t = Trigger {
id: "area".into(),
map_id: "map1".into(),
trigger_type: TriggerType::OnStep,
x: 4,
y: 4,
width: 2,
height: 2,
script_name: "areaFn".into(),
one_shot: false,
fired: false,
};
mgr.add_trigger(t);
mgr.check_triggers("map1", 0, 0);
for (x, y) in [(4, 4), (5, 4), (4, 5), (5, 5)] {
assert!(mgr.check_triggers("map1", x, y).contains(&"areaFn".into()));
}
mgr.check_triggers("map1", 0, 0);
assert!(!mgr.check_triggers("map1", 3, 4).contains(&"areaFn".into()));
assert!(!mgr.check_triggers("map1", 6, 4).contains(&"areaFn".into()));
}
#[test]
fn remove_triggers_for_map() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("a", "map1", TriggerType::OnStep, 0, 0, "fn1", false));
mgr.add_trigger(make_trigger("b", "map2", TriggerType::OnStep, 0, 0, "fn2", false));
mgr.remove_triggers_for_map("map1");
assert_eq!(mgr.len(), 1);
assert_eq!(mgr.triggers[0].script_name, "fn2");
}
#[test]
fn on_step_and_on_enter_both_fire_on_entry() {
let mut mgr = TriggerManager::new();
mgr.add_trigger(make_trigger("s", "map1", TriggerType::OnStep, 2, 3, "stepFn", false));
mgr.add_trigger(make_trigger("e", "map1", TriggerType::OnEnter, 2, 3, "enterFn", false));
mgr.check_triggers("map1", 0, 0); let names = mgr.check_triggers("map1", 2, 3);
assert!(names.contains(&"stepFn".into()));
assert!(names.contains(&"enterFn".into()));
}
}