timer.lua

Shipped with TRX as data/trx/ship/modules/timer.lua.

-- Runs a function once some ticks have passed, or again every few ticks.
--
-- A timer counts the ticks of one of two clocks. The game clock counts the
-- ticks in which the level runs its logic, so it stops while the game is
-- paused, in the inventory and in the menus. The interface clock counts every
-- tick, whatever is on screen, and is the clock for menus and fades.
--
-- A timer made by a level script stops when the level ends, as the event
-- handlers of a level script do. Timers are not written into a savegame.
--
-- Usage:
--   local timer = require("common.timer")
--   local alarm = timer.after(90, function() trx.log.info("time is up") end)
--   timer.every(30, blink, { clock = timer.Clock.UI })
--   alarm:cancel()

local M = {}

-- The clocks a timer counts.
M.Clock = {
  -- The ticks in which the level runs its logic.
  GAME = "game",
  -- Every tick.
  UI = "ui",
}

local HOOKS = {
  [M.Clock.GAME] = function(callback)
    return trx.events.before_control(callback)
  end,
  [M.Clock.UI] = function(callback)
    return trx.events.on_tick(callback)
  end,
}

---@class common.timer.Timer
---@field is_running boolean Whether the timer still has a tick to run on.
---@field private listener? trx.events.Listener
local Timer = {}
Timer.__index = Timer

-- Stops the timer, so that its function does not run again. Returns whether
-- the timer was still running.
function Timer:cancel()
  if self.listener == nil then
    return false
  end
  self.listener:detach()
  self.listener = nil
  self.is_running = false
  return true
end

-- Makes a timer that runs `fn` after `ticks` ticks, and again every `every`
-- ticks after that where `every` is given. The tick that makes the timer does
-- not count. An error in `fn` stops the timer, and is raised again.
local function start(ticks, every, fn, options)
  if math.type(ticks) ~= "integer" or ticks < 0 then
    error("the ticks must be a whole number of at least 0", 3)
  end
  local clock = options ~= nil and options.clock or M.Clock.GAME
  local hook = HOOKS[clock]
  if hook == nil then
    error("the clock is not one of common.timer.Clock", 3)
  end
  local self = setmetatable({ is_running = true }, Timer)
  local left = math.max(ticks, 1)
  self.listener = hook(function()
    left = left - 1
    if left > 0 then
      return
    end
    if every ~= nil then
      left = every
    else
      self:cancel()
    end
    local ok, err = pcall(fn)
    if not ok then
      self:cancel()
      error(err, 0)
    end
  end)
  return self
end

-- Runs `fn` once, `ticks` ticks from now. A timer of 0 ticks runs on the next
-- tick. `options.clock` picks the clock, `timer.Clock.GAME` by default.
function M.after(ticks, fn, options)
  return start(ticks, nil, fn, options)
end

-- Runs `fn` every `ticks` ticks, from `ticks` ticks from now, until the timer
-- is cancelled. `options.clock` picks the clock, `timer.Clock.GAME` by
-- default.
function M.every(ticks, fn, options)
  if math.type(ticks) ~= "integer" or ticks < 1 then
    error("the ticks must be a whole number of at least 1", 2)
  end
  return start(ticks, ticks, fn, options)
end

return M

Back to the shipped scripts