diff --git a/timer/timer.lua b/timer/timer.lua index 10ea0a1..9a300dd 100644 --- a/timer/timer.lua +++ b/timer/timer.lua @@ -4,7 +4,6 @@ local loaded, signal = pcall(require, (...):match("^(.-)timer").."signal") if not loaded then signal = nil end local ease = require((...):match("^.-timer")..".easing") -local timer --- Returns true if all the values in the list are true ; functions in the list will be called and the test will be performed on their return value. -- Returns default if the list is empty. @@ -24,222 +23,234 @@ local function all(list, default) end end +-- Timer methods. +local timer_mt = { + --- timer data table + t = nil, + + --- Wait time milliseconds before running the function. + -- Specify no time to remove condition. + after = function(self, time) + self.t.after = time + return self + end, + --- Run the function every time millisecond. + -- Specify no time to remove condition. + every = function(self, time) + self.t.every = time + return self + end, + --- The function will not execute more than count times. + -- Specify no time to remove condition. + times = function(self, count) + self.t.times = count or -1 + return self + end, + --- The TimedFunction will be active for a time duration. + -- Specify no time to remove condition. + during = function(self, time) + self.t.during = time + return self + end, + + --- Function conditions --- + --- Starts the function execution when func() returns true. Checked before the "after" condition, + -- meaning the "after" countdown starts when func() returns true. + -- If multiple init functions are added, init will trigger only when all of them returns true. + initWhen = function(self, func) + table.insert(self.t.initWhen, func) + return self + end, + --- Starts the function execution when func() returns true. Checked after the "after" condition. + -- If multiple start functions are added, start will trigger only when all of them returns true. + startWhen = function(self, func) + table.insert(self.t.startWhen, func) + return self + end, + --- When the functions ends, the execution won't stop and will repeat as long as func() returns true. + -- Will cancel timed repeat conditions if false but needs other timed repeat conditions to be true to create a new repeat. + -- If multiple repeat functions are added, a repeat will trigger only when all of them returns true. + repeatWhile = function(self, func) + table.insert(self.t.repeatWhile, func) + return self + end, + --- Stops the function execution when func() returns true. Checked before all timed conditions. + -- If multiple stop functions are added, stop will trigger only when all of them returns true. + stopWhen = function(self, func) + table.insert(self.t.stopWhen, func) + return self + end, + + --- Conditions override --- + --- Force the function to start its execution. + start = function(self) + self.t.forceStart = true + return self + end, + --- Force the function to stop its execution. + stop = function(self) + self.t.forceStop = true + return self + end, + --- Force the function to stop immediately. Won't trigger onEnd or other callbacks. + abort = function(self) + self.t.abort = true + return self + end, + --- Skip some amount of time. + skip = function(self, time) + self.t.skip = (self.t.skip or 0) + time + end, + + --- Callbacks functions --- + --- Will execute func(self, lag) when the function execution start. + onStart = function(self, func) + table.insert(self.t.onStart, func) + return self + end, + --- Will execute func(self, lag) each frame the main function is run. + onUpdate = function(self, func) + table.insert(self.t.onUpdate, func) + return self + end, + --- Will execute func(self, lag) when the function execution end. + onEnd = function(self, func) + table.insert(self.t.onEnd, func) + return self + end, + + --- Chaining --- + --- Creates another TimedFunction which will be initialized immediately when the current one ends. + -- Returns the new TimedFunction. + chain = function(self, func) + local done = false + local fn = self:run(func) + :initWhen(function() return done end) + self:onEnd(function(self, lag) + done = true + fn:skip(lag) + end) + return fn + end +} +timer_mt.__index = timer_mt + --- Registry methods. local registry_mt = { - --- Update all the TimedFunctions calls. + --- Update all the timers in the registry. -- Should be called at every game update; called by ubiquitousse.update. -- @tparam number dt the delta-time (time spent since last time the function was called) (miliseconds) -- @impl ubiquitousse update = function(self, dt) self.dt = dt - local done = {} -- functions done running - - local d = self.delayed - for func, t in pairs(d) do - if t and all(t.initWhen, true) then - t.initWhen = {} - local co = t.coroutine - -- skip - dt = self.dt - if t.skip then dt = dt + t.skip end - -- start - if t.after then t.after = t.after - dt end - if t.forceStart or ((not t.after or t.after <= 0) and all(t.startWhen, true)) then - local startLag = 0 - if t.after then - startLag = -t.after - elseif t.skip then - startLag = t.skip - end - t.after, t.skip = nil, nil - t.startWhen = {} - d[func] = false -- niling here cause the next pair iteration to error - table.insert(done, func) - if not co then - co = coroutine.create(func) - t.coroutine = co - if t.times > 0 then t.times = t.times - 1 end - for _, f in ipairs(t.onStart) do f(t.object, startLag) end - end - -- update - assert(coroutine.resume(co, function(delay) - t.after = delay - startLag - d[func] = t - coroutine.yield() - end, dt)) - for _, f in ipairs(t.onUpdate) do f(t.object, startLag) end - if t.during then t.during = t.during - startLag - dt end - -- stopping / repeat - if all(t.stopWhen, false) then t.forceStop = true end - if t.forceStop or coroutine.status(co) == "dead" then - if t.forceStop - or (t.during and t.during <= 0) - or (t.times == 0) - or (not all(t.repeatWhile, true)) - or (t.every == nil and t.times == -1 and t.during == nil and #t.repeatWhile == 0) -- no repeat - then - local endLag = t.during and -t.during or 0 - for _, f in ipairs(t.onEnd) do f(t.object, endLag) end - else + -- process timers + for _, timer in ipairs(self.delayed) do + local t = timer.t + if not t.dead then + if t.abort then + t.dead = true + elseif all(t.initWhen, true) then + t.initWhen = {} + local co = t.coroutine + -- skip + dt = self.dt + if t.skip then dt = dt + t.skip end + -- start + if t.after then t.after = t.after - dt end + if t.forceStart or ((not t.after or t.after <= 0) and all(t.startWhen, true)) then + local startLag = 0 + if t.after then + startLag = -t.after + elseif t.skip then + startLag = t.skip + end + t.after, t.skip = nil, nil + t.startWhen = {} + t.dead = true -- niling here cause the next pair iteration to error + if not co then + co = coroutine.create(t.func) + t.coroutine = co if t.times > 0 then t.times = t.times - 1 end - if t.every then t.after = t.every - startLag end - t.coroutine = coroutine.create(func) - d[func] = t + for _, f in ipairs(t.onStart) do f(timer, startLag) end + end + -- update + assert(coroutine.resume(co, timer, dt, 0, function(delay) + t.after = delay - startLag + t.dead = false + local _, _, dt, lag = coroutine.yield() + return dt, lag + end)) + for _, f in ipairs(t.onUpdate) do f(t.object, startLag) end + if t.during then t.during = t.during - startLag - dt end + -- stopping / repeat + if all(t.stopWhen, false) then t.forceStop = true end + if t.forceStop or coroutine.status(co) == "dead" then + if t.forceStop + or (t.during and t.during <= 0) + or (t.times == 0) + or (not all(t.repeatWhile, true)) + or (t.every == nil and t.times == -1 and t.during == nil and #t.repeatWhile == 0) -- no repeat + then + local endLag = t.during and -t.during or 0 + for _, f in ipairs(t.onEnd) do f(timer, endLag) end + else + if t.times > 0 then t.times = t.times - 1 end + if t.every then t.after = t.every - startLag end + t.coroutine = coroutine.create(t.func) + t.dead = false + end end end end end end - for _, func in ipairs(done) do - if not d[func] then - d[func] = nil + -- remove dead timers + for i=#self.delayed, 1, -1 do + if self.delayed[i].t.dead then + table.remove(self.delayed, i) end end end, --- Schedule a function to run. - -- The function will receive as first parameter the wait(time) function, which will pause the function execution for time miliseconds. + -- The function will receive as first parameter the TimedFunction object. -- As a second parameter, the function will receive the delta time (dt). -- As a third parameter, the function will receive the lag time (difference between the time when the function was run and when it should have been run). + -- As a fourth parameter, the function will receive as first parameter the wait(time) function, which will pause the function execution for time miliseconds. -- @tparam[opt] function func the function to schedule - -- @treturn TimedFunction the object + -- @treturn timer the object -- @impl ubiquitousse run = function(self, func) - -- Creates empty function (the TimedFunction may be used for time measure or stuff like that which doesn't need a specific function) - func = func or function() end + local r = setmetatable({ + t = { + dead = false, + func = func or function() end, + coroutine = nil, - -- Since delayed functions can end in any order, it doesn't really make sense to use a integer-keyed list. - -- Using the function as the key works and it's unique. - self.delayed[func] = { - object = nil, - coroutine = nil, + after = nil, + every = nil, + times = -1, + during = nil, - after = nil, - every = nil, - times = -1, - during = nil, + initWhen = {}, + startWhen = {}, + repeatWhile = {}, + stopWhen = {}, - initWhen = {}, - startWhen = {}, - repeatWhile = {}, - stopWhen = {}, + forceStart = false, + forceStop = false, + skip = nil, - forceStart = false, - forceStop = false, - skip = nil, + onStart = {}, + onUpdate = {}, + onEnd = {} + } + }, timer_mt) - onStart = {}, - onUpdate = {}, - onEnd = {} - } + table.insert(self.delayed, r) - local t = self.delayed[func] -- internal data - local r -- external interface - r = { - --- Timed conditions --- - --- Wait time milliseconds before running the function. - -- Specify no time to remove condition. - after = function(_, time) - t.after = time - return r - end, - --- Run the function every time millisecond. - -- Specify no time to remove condition. - every = function(_, time) - t.every = time - return r - end, - --- The function will not execute more than count times. - -- Specify no time to remove condition. - times = function(_, count) - t.times = count or -1 - return r - end, - --- The TimedFunction will be active for a time duration. - -- Specify no time to remove condition. - during = function(_, time) - t.during = time - return r - end, - - --- Function conditions --- - --- Starts the function execution when func() returns true. Checked before the "after" condition, - -- meaning the "after" countdown starts when func() returns true. - -- If multiple init functions are added, init will trigger only when all of them returns true. - initWhen = function(_, func) - table.insert(t.initWhen, func) - return r - end, - --- Starts the function execution when func() returns true. Checked after the "after" condition. - -- If multiple start functions are added, start will trigger only when all of them returns true. - startWhen = function(_, func) - table.insert(t.startWhen, func) - return r - end, - --- When the functions ends, the execution won't stop and will repeat as long as func() returns true. - -- Will cancel timed repeat conditions if false but needs other timed repeat conditions to be true to create a new repeat. - -- If multiple repeat functions are added, a repeat will trigger only when all of them returns true. - repeatWhile = function(_, func) - table.insert(t.repeatWhile, func) - return r - end, - --- Stops the function execution when func() returns true. Checked before all timed conditions. - -- If multiple stop functions are added, stop will trigger only when all of them returns true. - stopWhen = function(_, func) - table.insert(t.stopWhen, func) - return r - end, - - --- Conditions override --- - --- Force the function to start its execution. - start = function(_) - t.forceStart = true - return r - end, - --- Force the function to stop its execution. - stop = function(_) - t.forceStop = true - return r - end, - --- Skip some amount of time. - skip = function(_, time) - t.skip = (t.skip or 0) + time - end, - - --- Callbacks functions --- - --- Will execute func(self, lag) when the function execution start. - onStart = function(_, func) - table.insert(t.onStart, func) - return r - end, - --- Will execute func(self, lag) each frame the main function is run. - onUpdate = function(_, func) - table.insert(t.onUpdate, func) - return r - end, - --- Will execute func(self, lag) when the function execution end. - onEnd = function(_, func) - table.insert(t.onEnd, func) - return r - end, - - --- Chaining --- - --- Creates another TimedFunction which will be initialized immediately when the current one ends. - -- Returns the new TimedFunction. - chain = function(_, func) - local done = false - local fn = self:run(func) - :initWhen(function() return done end) - r:onEnd(function(self, lag) - done = true - fn:skip(lag) - end) - return fn - end - } - t.object = r return r end, @@ -271,7 +282,7 @@ local registry_mt = { end end - local r = self:run(function(wait, dt) + local r = self:run(function(self, dt) time = time + dt update(tbl, from, to) end):during(duration) @@ -325,6 +336,7 @@ local registry_mt = { registry_mt.__index = registry_mt --- Time related functions +local timer timer = { --- Creates and return a new TimerRegistry. -- A TimerRegistry is a separate ubiquitousse.time instance: its TimedFunctions will be independant