starlit  Artifact [2bec98314f]

Artifact 2bec98314fcf5a2e64370890985fc4f379a836c3de184595f2abdf46be509fda:


-- 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 = starsoul.logger 'effect'
local lib = starsoul.mod.lib

-- FIXME saving object refs is iffy, find a better alternative
starsoul.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

starsoul.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(starsoul.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
starsoul.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(starsoul.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(starsoul.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 starsoul.effect.active[s.i] = nil else
			for k,v in pairs(starsoul.effect.active) do
				if v == effect then starsoul.effect.active[k] = nil break end
			end
		end
	end
end

starsoul.effect.ensorcelled = function(player,effect)
	if type(player) == 'string' then player = minetest.get_player_by_name(player) end
	for _,s in pairs(starsoul.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

starsoul.effect.each = function(player,effect)
	local idx = 0
	return function()
		repeat idx = idx + 1
			local sp = starsoul.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 >= #starsoul.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

starsoul.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)
			-- starsoul 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 starsoul.effect.probe(s.caster:get_pos()).disjunction) or
				 (s.anchor and starsoul.effect.probe(s.anchor,s.range).disjunction)) then
				starsoul.effect.disjoin{effect=s}
			else
				if not s.disjunction then for _,sub in pairs(s.subjects) do
					local sp = sub.player:get_pos()
					if starsoul.effect.probe(sp).disjunction then
						starsoul.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 = #starsoul.effect.active+1
	s.cancel = function()
		s.abort()
		starsoul.effect.active[myid] = nil
	end
	local perform_disjunction_calls = function()
		local positions = get_effect_positions(s)
		for _,p in pairs(positions) do
			starsoul.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
						starsoul.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
	starsoul.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
			starsoul.effect.active[myid] = nil
		end)
	end
	s.starttime = minetest.get_server_uptime()
	return s
end

minetest.register_on_dieplayer(function(player)
	starsoul.effect.disjoin{target=player}
end)