diff --git a/.luacheckrc b/.luacheckrc
index 8142729..1221c0b 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -5,6 +5,7 @@ globals = {
"Focus",
"Day",
"Timer",
+ "Trigger",
"Util",
"Decision",
"Situation",
diff --git a/impostor.inc b/impostor.inc
index ca29ee2..126d176 100644
--- a/impostor.inc
+++ b/impostor.inc
@@ -9,6 +9,7 @@ logic/logic.meter.lua
logic/logic.focus.lua
logic/logic.day.lua
logic/logic.timer.lua
+logic/logic.trigger.lua
logic/logic.minigame.lua
system/system.ui.lua
audio/audio.manager.lua
diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua
index dbc0bed..7434246 100644
--- a/inc/init/init.context.lua
+++ b/inc/init/init.context.lua
@@ -18,6 +18,7 @@ Context = {}
--- * minigame_button_mash (table) Button mash minigame state (see Minigame.get_default_button_mash).
--- * minigame_rhythm (table) Rhythm minigame state (see Minigame.get_default_rhythm).
--- * meters (table) Meter values (see Meter.get_initial).
+--- * triggers (table) Active trigger runtime state, keyed by trigger ID.
--- * stat_screen_active (boolean) Whether the stat screen overlay is currently shown.
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID, `current_situation` (string|nil) active situation ID.
function Context.initial_data()
@@ -35,6 +36,7 @@ function Context.initial_data()
minigame_rhythm = {},
meters = Meter.get_initial(),
timer = Timer.get_initial(),
+ triggers = {},
game = {
current_screen = "home",
current_situation = nil,
diff --git a/inc/init/init.module.lua b/inc/init/init.module.lua
index d99472a..a1b9759 100644
--- a/inc/init/init.module.lua
+++ b/inc/init/init.module.lua
@@ -13,4 +13,5 @@ Sprite = {}
Audio = {}
Focus = {}
Day = {}
-Timer = {}
\ No newline at end of file
+Timer = {}
+Trigger = {}
\ No newline at end of file
diff --git a/inc/logic/logic.trigger.lua b/inc/logic/logic.trigger.lua
new file mode 100644
index 0000000..7f15c8a
--- /dev/null
+++ b/inc/logic/logic.trigger.lua
@@ -0,0 +1,135 @@
+--- @section Trigger
+local triggers = {}
+
+--- @within Trigger
+--- @param trigger table The trigger data table.
+--- @param trigger.id string Unique trigger identifier.
+--- @param trigger.duration number Duration in frames before the trigger fires.
+--- @param[opt] trigger.on_start function Called when the trigger starts. Defaults to noop.
+--- @param[opt] trigger.on_stop function Called when the trigger fires or is manually stopped. Defaults to noop.
+--- @param[opt] trigger.repeating boolean If true, trigger restarts after firing. Defaults to false.
+function Trigger.register(trigger)
+ if not trigger or not trigger.id then
+ trace("Error: Invalid trigger registered (missing id)!")
+ return
+ end
+ if not trigger.duration or trigger.duration <= 0 then
+ trace("Error: Invalid trigger registered (missing or invalid duration)!")
+ return
+ end
+
+ if not trigger.on_start then
+ trigger.on_start = function() end
+ end
+ if not trigger.on_stop then
+ trigger.on_stop = function() end
+ end
+ if trigger.repeating == nil then
+ trigger.repeating = false
+ end
+ if triggers[trigger.id] then
+ trace("Warning: Overwriting trigger with id: " .. trigger.id)
+ end
+ triggers[trigger.id] = trigger
+end
+
+--- @within Trigger
+--- @param id string The trigger ID.
+--- @return table|nil result The trigger definition or nil.
+function Trigger.get_by_id(id)
+ return triggers[id]
+end
+
+--- @within Trigger
+--- @return table result All trigger definitions keyed by ID.
+function Trigger.get_all()
+ return triggers
+end
+
+--- @within Trigger
+--- @param id string The trigger ID.
+--- @return boolean active True if the trigger is running.
+function Trigger.is_active(id)
+ if not Context or not Context.triggers then return false end
+ return Context.triggers[id] ~= nil
+end
+
+--- If already active, restarts from 0.
+--- @within Trigger
+--- @param id string The trigger ID.
+function Trigger.start(id)
+ if not Context or not Context.triggers then return end
+ local trigger = triggers[id]
+ if not trigger then
+ trace("Error: Cannot start unknown trigger: " .. tostring(id))
+ return
+ end
+
+ Context.triggers[id] = { elapsed = 0 }
+ trigger.on_start()
+end
+
+--- @within Trigger
+--- @param id string The trigger ID.
+function Trigger.stop(id)
+ if not Context or not Context.triggers then return end
+ local trigger = triggers[id]
+ if not trigger then
+ trace("Error: Cannot stop unknown trigger: " .. tostring(id))
+ return
+ end
+ if not Context.triggers[id] then return end
+
+ Context.triggers[id] = nil
+ trigger.on_stop()
+end
+
+--- Resets elapsed time to 0 without calling handlers. No-op if inactive.
+--- @within Trigger
+--- @param id string The trigger ID.
+function Trigger.reset(id)
+ if not Context or not Context.triggers then return end
+ if not triggers[id] then
+ trace("Error: Cannot reset unknown trigger: " .. tostring(id))
+ return
+ end
+ if not Context.triggers[id] then return end
+
+ Context.triggers[id].elapsed = 0
+end
+
+--- Pauses during minigames.
+--- @within Trigger
+function Trigger.update()
+ if not Context or not Context.game_in_progress or not Context.triggers then return end
+
+ local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
+ if in_minigame then return end
+
+ local fired = {}
+ for id, state in pairs(Context.triggers) do
+ local trigger = triggers[id]
+ if trigger then
+ state.elapsed = state.elapsed + 1
+ if state.elapsed >= trigger.duration then
+ table.insert(fired, id)
+ end
+ else
+ table.insert(fired, id)
+ end
+ end
+
+ for _, id in ipairs(fired) do
+ local trigger = triggers[id]
+ if trigger then
+ trigger.on_stop()
+ if trigger.repeating then
+ Context.triggers[id] = { elapsed = 0 }
+ else
+ Context.triggers[id] = nil
+ end
+ else
+ Context.triggers[id] = nil
+ end
+ end
+end
diff --git a/inc/system/system.main.lua b/inc/system/system.main.lua
index 1204d02..9140532 100644
--- a/inc/system/system.main.lua
+++ b/inc/system/system.main.lua
@@ -23,6 +23,7 @@ function TIC()
end
Meter.update()
Timer.update()
+ Trigger.update()
if Context.game_in_progress then
Meter.draw()
Timer.draw()