-- ported from sorcery/spell.lua, hence the lingering refs to "magic"
--
-- this file is used to track active effects, for the purposes of metamagic
-- like disjunction. a "effect" is a table consisting of several properties:
-- a "disjoin" function that, if present, is called when the effect is
-- abnormally interrupted, a "terminate" function that calls when the effect
-- completes, a "duration" property specifying how long the effect lasts in
-- seconds, and a "timeline" table that maps floats to functions called at
-- specific points during the function's activity. it can also have a
-- 'delay' property that specifies how long to wait until the effect sequence
-- starts; the effect is however still vulnerable to disjunction during this
-- period. there can also be a sounds table that maps timepoints to sounds
-- the same way timeline does. each value should be a table of form {sound,
-- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or
-- a vector specifying a position in the world, and indicate where the sound
-- should be played. by default 'caster' and 'subjects' sounds will be attached
-- to the objects they reference; 'attach=false' can be added to prevent this.
-- by default sounds will be faded out quickly when disjunction occurs; this
-- can be controlled by the fade parameter.
--
-- effects can have various other properties, for instance 'disjunction', which
-- when true prevents other effects from being cast in its radius while it is
-- still in effect. disjunction is absolute; there is no way to overwhelm it.
--
-- the effect also needs at least one of "anchor", "subjects", or "caster".
-- * an anchor is a position that, in combination with 'range', specifies the area
-- where a effect is in effect; this is used for determining whether it
-- is affected by a disjunction that incorporates part of that position
-- * subjects is an array of individuals affected by the effect. when
-- disjunction is cast on one of them, they will be removed from the
-- table. each entry should have at least a 'player' field; they can
-- also contain any other data useful to the effect. if a subject has
-- a 'disjoin' field it must be a function called when they are removed
-- from the list of effect targets.
-- * caster is the individual who cast the effect, if any. a disjunction
-- against their person will totally disrupt the effect.
local log = starlit.logger 'effect'
local lib = starlit.mod.lib
-- FIXME saving object refs is iffy, find a better alternative
starlit.effect = {
active = {}
}
local get_effect_positions = function(effect)
local effectpos
if effect.anchor then
effectpos = {effect.anchor}
elseif effect.attach then
if effect.attach == 'caster' then
effectpos = {effect.caster:get_pos()}
elseif effect.attach == 'subjects' or effect.attach == 'both' then
if effect.attach == 'both' then
effectpos = {effect.caster:get_pos()}
else effectpos = {} end
for _,s in pairs(effect.subjects) do
effectpos[#effectpos+1] = s.player:get_pos()
end
else effectpos = {effect.attach:get_pos()} end
else assert(false) end
return effectpos
end
local ineffectrange = function(effect,pos,range)
local effectpos = get_effect_positions(effect)
for _,p in pairs(effectpos) do
if vector.equals(pos,p) or
(range and lib.math.vdcomp(range, pos,p)<=1) or
(effect.range and lib.math.vdcomp(effect.range,p,pos)<=1) then
return true
end
end
return false
end
starlit.effect.probe = function(pos,range)
-- this should be called before any effects are performed.
-- other mods can overlay their own functions to e.g. protect areas
-- from effects
local result = {}
-- first we need to check if any active injunctions are in effect
-- injunctions are registered as effects with a 'disjunction = true'
-- property
for id,effect in pairs(starlit.effect.active) do
if not (effect.disjunction and (effect.anchor or effect.attach)) then goto skip end
if ineffectrange(effect,pos,range) then
result.disjunction = true
break
end
::skip::end
-- at some point we might also check to see if certain anti-effect
-- blocks are nearby or suchlike. there could also be regions where
-- perhaps certain kinds of effect are unusually empowered or weak
return result
end
starlit.effect.disjoin = function(d)
local effects,targets = {},{}
if d.effect then effects = {{v=d.effect}}
elseif d.target then targets = {d.target}
elseif d.pos then -- find effects anchored here and people in range
for id,effect in pairs(starlit.effect.active) do
if not effect.anchor then goto skip end -- this intentionally excludes attached effects
if ineffectrange(effect,d.pos,d.range) then
effects[#effects+1] = {v=effect,i=id}
end
::skip::end
local ppl = minetest.get_objects_inside_radius(d.pos,d.range)
if #targets == 0 then targets = ppl else
for _,p in pairs(ppl) do targets[#targets+1] = p end
end
end
-- iterate over targets to remove from any effect's influence
for _,t in pairs(targets) do
for id,effect in pairs(starlit.effect.active) do
if effect.caster == t then effects[#effects+1] = {v=effect,i=id} else
for si, sub in pairs(effect.subjects) do
if sub.player == t then
if sub.disjoin then sub:disjoin(effect) end
effect.release_subject(si)
break
end
end
end
end
end
-- effects to disjoin entirely
for _,s in pairs(effects) do local effect = s.v
if effect.disjoin then effect:disjoin() end
effect.abort()
if s.i then starlit.effect.active[s.i] = nil else
for k,v in pairs(starlit.effect.active) do
if v == effect then starlit.effect.active[k] = nil break end
end
end
end
end
starlit.effect.ensorcelled = function(player,effect)
if type(player) == 'string' then player = minetest.get_player_by_name(player) end
for _,s in pairs(starlit.effect.active) do
if effect and (s.name ~= effect) then goto skip end
for _,sub in pairs(s.subjects) do
if sub.player == player then return s end
end
::skip::end
return false
end
starlit.effect.each = function(player,effect)
local idx = 0
return function()
repeat idx = idx + 1
local sp = starlit.effect.active[idx]
if sp == nil then return nil end
if effect == nil or sp.name == effect then
for _,sub in pairs(sp.subjects) do
if sub.player == player then return sp end
end
end
until idx >= #starlit.effect.active
end
end
-- when a new effect is created, we analyze it and make the appropriate calls
-- to minetest.after to queue up the events. each job returned needs to be
-- saved in 'jobs' so they can be canceled if the effect is disjoined. no polling
-- necessary :D
starlit.effect.cast = function(proto)
local s = table.copy(proto)
s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {}
s.impacts = s.impacts or {} s.subjects = s.subjects or {}
s.delay = s.delay or 0
s.visual = function(subj, def)
s.vfx[#s.vfx + 1] = {
handle = minetest.add_particlespawner(def);
subject = subj;
}
end
s.visual_caster = function(def) -- convenience function
local d = table.copy(def)
d.attached = s.caster
s.visual(nil, d)
end
s.visual_subjects = function(def)
for _,sub in pairs(s.subjects) do
local d = table.copy(def)
d.attached = sub.player
s.visual(sub, d)
end
end
s.affect = function(i)
local etbl = {}
for _,sub in pairs(s.subjects) do
-- local eff = late.new_effect(sub.player, i)
-- starlit will not be using late
local rec = {
effect = eff;
subject = sub;
}
s.impacts[#s.impacts+1] = rec
etbl[#etbl+1] = rec
end
return etbl
end
s.abort = function()
for _,j in ipairs(s.jobs) do j:cancel() end
for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end
for _,i in ipairs(s.sfx) do s.silence(i) end
for _,i in ipairs(s.impacts) do i.effect:stop() end
end
s.release_subject = function(si)
local t = s.subjects[si]
for _,f in pairs(s.sfx) do if f.subject == t then s.silence(f) end end
for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
for _,f in pairs(s.vfx) do
if f.subject == t then minetest.delete_particlespawner(f.handle) end
end
s.subjects[si] = nil
end
local interpret_timespec = function(when)
if when == nil then return 0 end
local t if type(when) == 'number' then
t = s.duration * when
else
t = (s.duration * (when.whence or 0)) + (when.secs or 0)
end
if t then return math.min(s.duration,math.max(0,t)) end
log.err('invalid timespec ' .. dump(when))
return 0
end
s.queue = function(when,fn)
local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0
local timepast = interpret_timespec(when)
if not timepast then timepast = 0 end
local timeleft = s.duration - timepast
local howlong = (s.delay + timepast) - elapsed
if howlong < 0 then
log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
howlong = 0
end
s.jobs[#s.jobs+1] = minetest.after(howlong, function()
-- this is somewhat awkward. since we're using a non-polling approach, we
-- need to find a way to account for a caster or subject walking into an
-- existing antimagic field, or someone with an existing antimagic aura
-- walking into range of the anchor. so every time a effect effect would
-- take place, we first check to see if it's in range of something nasty
if not s.disjunction and -- avoid self-disjunction
((s.caster and starlit.effect.probe(s.caster:get_pos()).disjunction) or
(s.anchor and starlit.effect.probe(s.anchor,s.range).disjunction)) then
starlit.effect.disjoin{effect=s}
else
if not s.disjunction then for _,sub in pairs(s.subjects) do
local sp = sub.player:get_pos()
if starlit.effect.probe(sp).disjunction then
starlit.effect.disjoin{pos=sp}
end
end end
-- effect still exists and we've removed any subjects who have been
-- affected by a disjunction effect, it's now time to actually perform
-- the queued-up action
fn(s,timepast,timeleft)
end
end)
end
s.play_now = function(spec)
local specs, stbl = {}, {}
local addobj = function(obj,sub)
if spec.attach == false then specs[#specs+1] = {
spec = { pos = obj:get_pos() };
obj = obj, subject = sub;
} else specs[#specs+1] = {
spec = { object = obj };
obj = obj, subject = sub;
} end
end
if spec.where == 'caster' then addobj(s.caster)
elseif spec.where == 'subjects' then
for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end
elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} }
else specs[#specs+1] = { spec = {pos = spec.where} } end
for _,sp in pairs(specs) do
sp.spec.gain = sp.spec.gain or spec.gain
local so = {
handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral);
ctl = spec;
-- object = sp.obj;
subject = sp.subject;
}
stbl[#stbl+1] = so
s.sfx[#s.sfx+1] = so
end
return stbl
end
s.play = function(when,spec)
s.queue(when, function()
local snds = s.play_now(spec)
if spec.stop then
s.queue(spec.stop, function()
for _,snd in pairs(snds) do s.silence(snd) end
end)
end
end)
end
s.silence = function(sound)
if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle)
else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
end
local startqueued, termqueued = false, false
local myid = #starlit.effect.active+1
s.cancel = function()
s.abort()
starlit.effect.active[myid] = nil
end
local perform_disjunction_calls = function()
local positions = get_effect_positions(s)
for _,p in pairs(positions) do
starlit.effect.disjoin{pos = p, range = s.range}
end
end
if s.timeline then
for when_raw,what in pairs(s.timeline) do
local when = interpret_timespec(when_raw)
if s.delay == 0 and when == 0 then
startqueued = true
if s.disjunction then perform_disjunction_calls() end
what(s,0,s.duration)
elseif when_raw == 1 or when >= s.duration then -- avoid race conditions
if not termqueued then
termqueued = true
s.queue(1,function(s,...)
what(s,...)
if s.terminate then s:terminate() end
starlit.effect.active[myid] = nil
end)
else
log.warn('multiple final timeline events not possible, ignoring')
end
elseif when == 0 and s.disjunction then
startqueued = true
s.queue(when_raw,function(...)
perform_disjunction_calls()
what(...)
end)
else s.queue(when_raw,what) end
end
end
if s.intervals then
for _,int in pairs(s.intervals) do
local timeleft = s.duration - interpret_timespec(int.after)
local iteration, itercount = 0, timeleft / int.period
local function iterate(lastreturn)
iteration = iteration + 1
local nr = int.fn {
effect = s;
iteration = iteration;
iterationcount = itercount;
timeleft = timeleft;
timeelapsed = s.duration - timeleft;
lastreturn = lastreturn;
}
if nr ~= false and iteration < itercount then
s.jobs[#s.jobs+1] = minetest.after(int.period,
function() iterate(nr) end)
end
end
if int.after
then s.queue(int.after, iterate)
else s.queue({whence=0, secs=s.period}, iterate)
end
end
end
if s.disjunction and not startqueued then
if s.delay == 0 then perform_disjunction_calls() else
s.queue(0, function() perform_disjunction_calls() end)
end
end
if s.sounds then
for when,what in pairs(s.sounds) do s.play(when,what) end
end
starlit.effect.active[myid] = s
if not termqueued then
s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function()
if s.terminate then s:terminate() end
starlit.effect.active[myid] = nil
end)
end
s.starttime = minetest.get_server_uptime()
return s
end
minetest.register_on_dieplayer(function(player)
starlit.effect.disjoin{target=player}
end)