Skip to content
Snippets Groups Projects
inscriptions.lua 60.3 KiB
Newer Older
dg's avatar
dg committed
-- ToME - Tales of Maj'Eyal
DarkGod's avatar
DarkGod committed
-- Copyright (C) 2009 - 2019 Nicolas Casalini
dg's avatar
dg committed
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org

dg's avatar
dg committed
newInscription = function(t)
dg's avatar
dg committed
	-- Warning, up that if more than 5 inscriptions are ever allowed
dg's avatar
dg committed
		local tt = table.clone(t)
		tt.short_name = tt.name:upper():gsub("[ ]", "_").."_"..i
dg's avatar
dg committed
		tt.display_name = function(self, t)
			local data = self:getInscriptionData(t.short_name)
			if data.item_name then
				local n = tstring{t.name, " ["}
				n:merge(data.item_name)
				n:add("]")
				return n
			else
				return t.name
			end
		end
		if tt.type[1] == "inscriptions/infusions" then tt.auto_use_check = function(self, t) return not self:hasEffect(self.EFF_INFUSION_COOLDOWN) end
		elseif tt.type[1] == "inscriptions/runes" then tt.auto_use_check = function(self, t) return not self:hasEffect(self.EFF_RUNE_COOLDOWN) end
		elseif tt.type[1] == "inscriptions/taints" then tt.auto_use_check = function(self, t) return not self:hasEffect(self.EFF_TAINT_COOLDOWN) end
		end
Otowa Kotori's avatar
Otowa Kotori committed
		tt.auto_use_warning = _t"- will only auto use when no saturation effect exists"
dg's avatar
dg committed
		tt.cooldown = function(self, t)
			local data = self:getInscriptionData(t.short_name)
			return data.cooldown
		end
		tt.old_info = tt.info
		tt.info = function(self, t)
			local ret = t.old_info(self, t)
			local data = self:getInscriptionData(t.short_name)
			if data.use_stat and data.use_stat_mod then
Otowa Kotori's avatar
Otowa Kotori committed
				ret = ret..("\nIts effects scale with your %s stat."):tformat(self.stats_def[data.use_stat].name)
			end
			return ret
		end
dg's avatar
dg committed
		if not tt.image then
			tt.image = "talents/"..(t.short_name or t.name):lower():gsub("[^a-z0-9_]", "_")..".png"
		end
		tt.is_inscription = true
dg's avatar
dg committed
		newTalent(tt)
	end
end

dg's avatar
dg committed
-----------------------------------------------------------------------
-- Infusions
-----------------------------------------------------------------------
dg's avatar
dg committed
newInscription{
	name = "Infusion: Regeneration",
	type = {"inscriptions/infusions", 1},
	points = 1,
	on_pre_use = function(self, t) return not self:hasEffect(self.EFF_REGENERATION) end,
	no_break_stealth = true,
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		self:setEffect(self.EFF_REGENERATION, data.dur, {power=(data.heal + data.inc_stat) / data.dur})
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[Activate the infusion to heal yourself for %d life over %d turns.]]):tformat(data.heal + data.inc_stat, data.dur)
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[heal %d; %d cd]]):tformat(data.heal + data.inc_stat, data.cooldown)
dg's avatar
dg committed
}

dg's avatar
dg committed
newInscription{
	name = "Infusion: Healing",
	type = {"inscriptions/infusions", 1},
	points = 1,
	tactical = { HEAL = 1,
		CURE = function(self, t, target)
Chris Davidson's avatar
Chris Davidson committed
			local cut, poison, disease = 0, 0, 0
			for eff_id, p in pairs(self.tmp) do
				local e = self.tempeffect_def[eff_id]
				if e.status == "detrimental" then
					if e.subtype.wound then cut = 1 end
					if e.subtype.poison then poison = 1 end
Chris Davidson's avatar
Chris Davidson committed
					if e.subtype.disease then disease = 1 end
Chris Davidson's avatar
Chris Davidson committed
			return cut + poison + disease
DarkGod's avatar
DarkGod committed
	is_heal = true, ignore_is_heal_test = true,
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		self:attr("allow_on_heal", 1)
		self:attr("disable_ancestral_life", 1)
DarkGod's avatar
DarkGod committed
		self:heal(data.heal + data.inc_stat, t)
		self:attr("disable_ancestral_life", -1)
		self:attr("allow_on_heal", -1)
		self:removeEffectsFilter(self, function(e) return e.subtype.wound end, 1)
		self:removeEffectsFilter(self, function(e) return e.subtype.poison end, 1)
		self:removeEffectsFilter(self, function(e) return e.subtype.disease end, 1)
		if core.shader.active(4) then
DarkGod's avatar
DarkGod committed
			self:addParticles(Particles.new("shader_shield_temp", 1, {toback=true , size_factor=1.5, y=-0.3, img="healgreen", life=25}, {type="healing", time_factor=2000, beamsCount=20, noup=2.0}))
			self:addParticles(Particles.new("shader_shield_temp", 1, {toback=false, size_factor=1.5, y=-0.3, img="healgreen", life=25}, {type="healing", time_factor=2000, beamsCount=20, noup=1.0}))
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[Activate the infusion to instantly heal yourself for %d then cleanse 1 wound, poison, and disease effect.]]):tformat(data.heal + data.inc_stat)
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[heal %d; cd %d]]):tformat(data.heal + data.inc_stat, data.cooldown)
dg's avatar
dg committed
}

dg's avatar
dg committed
newInscription{
dg's avatar
dg committed
	name = "Infusion: Wild",
dg's avatar
dg committed
	type = {"inscriptions/infusions", 1},
	points = 1,
dg's avatar
dg committed
	no_energy = true,
		CURE = function(self, t, target)
			local data = self:getInscriptionData(t.short_name)
			return #self:effectsFilter({types=data.what, status="detrimental"})
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)

		local target = self
		local effs = {}
		local force = {}
dg's avatar
dg committed

		removed = target:removeEffectsFilter(self, {types=data.what, subtype={["cross tier"] = true}, status="detrimental"})
		for k,v in pairs(data.what) do
			removed = removed + target:removeEffectsFilter(self, {type=k, status="detrimental"}, 1)
Otowa Kotori's avatar
Otowa Kotori committed
			game.logSeen(self, "%s is cured!", self:getName():capitalize())
dg's avatar
dg committed
		end
dg's avatar
dg committed
		self:setEffect(self.EFF_PAIN_SUPPRESSION, data.dur, {power=data.power + data.inc_stat})
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		local what = table.concatNice(table.ts(table.keys(data.what)), ", ", _t" and ")
Sebastian Vråle's avatar
Sebastian Vråle committed

		return ([[Activate the infusion to cure yourself of one random %s effect and reduce all damage taken by %d%% for %d turns.
Otowa Kotori's avatar
Otowa Kotori committed
Also removes cross-tier effects of the affected types for free.]]):tformat(what, data.power+data.inc_stat, data.dur)
dg's avatar
dg committed
	end,
Chris Davidson's avatar
Chris Davidson committed
	short_info = function(self, t)	
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		local what = table.concat(table.ts(table.keys(data.what)), ", ")
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[res %d%%; %s; dur %d; cd %d]]):tformat(data.power + data.inc_stat, what, data.dur, data.cooldown)
dg's avatar
dg committed
}

DarkGod's avatar
DarkGod committed
newInscription{
	name = "Infusion: Primal", image = "talents/infusion__wild.png",
	type = {"inscriptions/infusions", 1},
	points = 1,
	no_energy = true,
DarkGod's avatar
DarkGod committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		self:setEffect(self.EFF_PRIMAL_ATTUNEMENT, data.dur, {power=data.power + data.inc_stat*10, reduce=math.floor(data.reduce + data.inc_stat * 2)})
DarkGod's avatar
DarkGod committed
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return ([[Activate the infusion to heal for %d%% of all damage taken (calculated before resistances) and reduce the duration of a random debuff by %d each turn for %d turns.]]):
Otowa Kotori's avatar
Otowa Kotori committed
			tformat(data.power+data.inc_stat*10, math.floor((data.reduce or 0) + data.inc_stat * 2), data.dur)
DarkGod's avatar
DarkGod committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[affinity %d%%; reduction %d; dur %d; cd %d]]):tformat(data.power + data.inc_stat*10, math.floor((data.reduce or 0) + data.inc_stat * 2), data.dur, data.cooldown )
DarkGod's avatar
DarkGod committed
	end,
}

dg's avatar
dg committed
newInscription{
	name = "Infusion: Movement",
	type = {"inscriptions/infusions", 1},
	points = 1,
dg's avatar
dg committed
	no_energy = true,
	tactical = { ESCAPE = 1, CLOSEIN = 1 },
	on_pre_use = function(self, t) return not self:attr("never_move") end,
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
dg's avatar
dg committed
		game:onTickEnd(function() self:setEffect(self.EFF_WILD_SPEED, 1, {power=data.speed + data.inc_stat}) end)
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
dg's avatar
dg committed
		return ([[Activate the infusion to increase movement speed by %d%% for 1 game turn.
		You gain 100%% stun, daze, and pin immunity during the effect.
		Any actions other than movement will cancel the effect.
Otowa Kotori's avatar
Otowa Kotori committed
		Note: since you will be moving very fast, game turns will pass very slowly.]]):tformat(data.speed + data.inc_stat)
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[speed %d%%; cd %d]]):tformat(data.speed + data.inc_stat, data.cooldown)
dg's avatar
dg committed
}

dg's avatar
dg committed
newInscription{
dg's avatar
dg committed
	name = "Infusion: Heroism",
dg's avatar
dg committed
	type = {"inscriptions/infusions", 1},
	points = 1,
dg's avatar
dg committed
	no_energy = true,
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local bonus = 1 + (1 - math.max(0, self.life) / self.max_life)
		self:setEffect(self.EFF_HEROISM, math.floor(data.dur * bonus), {die_at=(data.die_at + data.inc_stat * 30) * bonus})
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local bonus = 1 + (1 -  math.max(0, self.life) / self.max_life)
		local bonus1 = (data.die_at + data.inc_stat * 30) * bonus
Chris Davidson's avatar
Chris Davidson committed
		local bonus2 = math.floor(data.dur * bonus)
		return ([[Activate the infusion to endure even the most grievous of wounds for %d turns.
DarkGod's avatar
DarkGod committed
		While Heroism is active, you will only die when reaching -%d life.
		The duration and life will increase by 1%% for every 1%% life you have lost, to a maximum of 100%% at 0 life or less (currently %d life, %d duration)
Otowa Kotori's avatar
Otowa Kotori committed
		If your life is below 0 when this effect wears off it will be set to 1.]]):tformat(data.dur, data.die_at + data.inc_stat * 30, bonus1, bonus2)
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[die at -%d; dur %d; cd %d]]):tformat(data.die_at + data.inc_stat * 30, data.dur, data.cooldown)
dg's avatar
dg committed
}

newInscription{
	name = "Infusion: Wild Growth",
	type = {"inscriptions/infusions", 1},
	points = 1,
	tactical = { ATTACKAREA = { PHYSICAL = 1, NATURE = 1 }, DEFEND = 1, DISABLE = {pin = 2}},
	range = 0,
	radius = 5,
	direct_hit = true,
	no_energy = true,
DarkGod's avatar
DarkGod committed
		return {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), selffire=false, friendlyfire = false, talent=t}
	getDamage = function(self, t) return 10 + self:combatMindpower() * 3.6 end,
		local data = self:getInscriptionData(t.short_name)
		local dam = t.getDamage(self, t)
		local tg = self:getTalentTarget(t)
		self:project(tg, self.x, self.y, function(tx, ty)
			DamageType:get(DamageType.ENTANGLE).projector(self, tx, ty, DamageType.ENTANGLE, dam)
		end)
		self:setEffect(self.EFF_THORNY_SKIN, data.dur, {hard=data.hard or 30, ac=data.armor or 50})
		game:playSoundNear(self, "talents/earth")
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local damage = t.getDamage(self, t)
		return ([[Causes thick vines to spring from the ground and entangle all targets within %d squares for %d turns, pinning them in place for 5 turns and dealing %0.2f physical damage and %0.2f nature damage.
		The vines also grow all around you, increasing your armour by %d and armour hardiness by %d.]]):
Otowa Kotori's avatar
Otowa Kotori committed
		tformat(self:getTalentRadius(t), data.dur, damDesc(self, DamageType.PHYSICAL, damage)/3, damDesc(self, DamageType.NATURE, 2*damage)/3, data.armor or 50, data.hard or 30)
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[rad %d; dur %d;]]):tformat(self:getTalentRadius(t), data.dur)
dg's avatar
dg committed
-----------------------------------------------------------------------
-- Runes
-----------------------------------------------------------------------
dg's avatar
dg committed

local function attack_rune(self, btid)
	for tid, lev in pairs(self.talents) do
		if tid ~= btid and self.talents_def[tid].is_attack_rune and not self.talents_cd[tid] then
			self.talents_cd[tid] = 1
		end
	end
end

dg's avatar
dg committed
newInscription{
	name = "Rune: Teleportation",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	no_break_stealth = true,
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		game.level.map:particleEmitter(self.x, self.y, 1, "teleport")
		self:teleportRandom(self.x, self.y, data.range + data.inc_stat, 15)
		game.level.map:particleEmitter(self.x, self.y, 1, "teleport")
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[Activate the rune to teleport randomly in a range of %d with a minimum range of 15.]]):tformat(data.range + data.inc_stat)
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[range %d; cd %d]]):tformat(data.range + data.inc_stat, data.cooldown)
dg's avatar
dg committed
}

dg's avatar
dg committed
newInscription{
	name = "Rune: Shielding",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
dg's avatar
dg committed
	no_energy = true,
dg's avatar
dg committed
	on_pre_use = function(self, t)
		return not self:hasEffect(self.EFF_DAMAGE_SHIELD)
	end,
	action = function(self, t)
dg's avatar
dg committed
		local data = self:getInscriptionData(t.short_name)
		self:setEffect(self.EFF_DAMAGE_SHIELD, data.dur, {power=data.power + data.inc_stat})
		return true
dg's avatar
dg committed
	end,
	info = function(self, t)
dg's avatar
dg committed
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[Activate the rune to create a protective shield absorbing at most %d damage for %d turns.]]):tformat(self:getShieldAmount(data.power + data.inc_stat), self:getShieldDuration(data.dur))
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[absorb %d; dur %d; cd %d]]):tformat(self:getShieldAmount(data.power + data.inc_stat), self:getShieldDuration(data.dur), data.cooldown)
dg's avatar
dg committed
}
dg's avatar
dg committed

newInscription{
	name = "Rune: Reflection Shield",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	allow_autocast = true,
	no_energy = true,
	tactical = { DEFEND = 2 },
Otowa Kotori's avatar
Otowa Kotori committed
	getPower = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		if data.power and data.inc_stat then 
			return data.power + data.inc_stat
		else
			return 100+5*self:getMag()
		end
	end,
	getDuration = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.dur or 5
	end,
	on_pre_use = function(self, t)
		return not self:hasEffect(self.EFF_DAMAGE_SHIELD)
	end,
	action = function(self, t)
Otowa Kotori's avatar
Otowa Kotori committed
		local power = t.getPower(self, t)
		local dur = t.getDuration(self, t)
		self:setEffect(self.EFF_DAMAGE_SHIELD, dur, {power=power, reflect=100, image="reflectionshield_17", shield_intensity=0.6})
		return true
	end,
	info = function(self, t)
Otowa Kotori's avatar
Otowa Kotori committed
		local power = t.getPower(self, t)
		local dur = t.getDuration(self, t)
		return ([[Activate the rune to create a protective shield absorbing and reflecting at most %d damage for %d turns.]]):tformat(self:getShieldAmount(power), self:getShieldDuration(dur))
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		local power = t.getPower(self, t)
		local dur = t.getDuration(self, t)
		return ([[absorb and reflect %d; dur %d; cd %d]]):tformat(self:getShieldAmount(power), self:getShieldDuration(dur), data.cd)
dg's avatar
dg committed
newInscription{
	name = "Rune: Biting Gale",
dg's avatar
dg committed
	type = {"inscriptions/runes", 1},
	points = 1,
	is_attack_rune = true,
	no_energy = true,
dg's avatar
dg committed
	is_spell = true,
	tactical = { ATTACK = { COLD = 1 }, DISABLE = { stun = 1 } },
dg's avatar
dg committed
	requires_target = true,
		return {type="cone", cone_angle=25, radius = self:getTalentRadius(t), range=self:getTalentRange(t), talent=t, display={particle="bolt_ice", trail="icetrail"}}
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
dg's avatar
dg committed
		local x, y = self:getTarget(tg)
		if not x or not y then return nil end
		local damage = data.power + data.inc_stat
		self:project(tg, x, y, function(tx, ty)
			local target = game.level.map(tx, ty, Map.ACTOR)
			if not target or target == self then return end
			
			DamageType:get(DamageType.COLD).projector(self, tx, ty, DamageType.COLD, damage)
			target:setEffect(target.EFF_WET, 5, {})
				target:setEffect(target.EFF_FROZEN, data.dur, {hp=damage*2})
		end, data.power + data.inc_stat, {type="freeze"})
dg's avatar
dg committed
		game:playSoundNear(self, "talents/ice")
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return ([[Activate the rune to direct a cone of chilling stormwind doing %0.2f cold damage.
Chris Davidson's avatar
Chris Davidson committed
			The storm will soak enemies hit reducing their resistance to stuns by 50%% then attempt to freeze them for %d turns.
			These effects can be resisted but not saved against.]]):
Otowa Kotori's avatar
Otowa Kotori committed
			tformat(damDesc(self, DamageType.COLD, data.power + data.inc_stat), data.dur)
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[damage %d; dur %d; cd %d]]):tformat(damDesc(self, DamageType.COLD, data.power + data.inc_stat), data.dur, data.cooldown)
dg's avatar
dg committed
}

newInscription{
	name = "Rune: Acid Wave",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_attack_rune = true,
	no_energy = true,
dg's avatar
dg committed
	is_spell = true,
	tactical = {
		ATTACKAREA = { ACID = 1 },
dg's avatar
dg committed
	requires_target = true,
	direct_hit = true,
		return {type="cone", radius=self:getTalentRadius(t), range=self:getTalentRange(t), selffire=false, cone_angle=25, talent=t}
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local tg = self:getTalentTarget(t)
		local x, y = self:getTarget(tg)
		if not x or not y then return nil end

		self:project(tg, x, y, function(tx, ty)
			local target = game.level.map(tx, ty, Map.ACTOR)
			if not target or target == self then return end
				target:setEffect(target.EFF_DISARMED, data.dur, {})
			DamageType:get(DamageType.ACID).projector(self, tx, ty, DamageType.ACID, data.power + data.inc_stat)
		end)

		game.level.map:particleEmitter(self.x, self.y, tg.radius, "breath_acid", {radius=tg.radius, tx=x-self.x, ty=y-self.y})
dg's avatar
dg committed
		game:playSoundNear(self, "talents/slime")
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		  local data = self:getInscriptionData(t.short_name)
		  return ([[Activate the rune to unleash a cone dealing %0.2f acid damage.
			The corrosive acid will also disarm enemies struck for %d turns.
			This effect can be resisted but not saved against.]]):
Otowa Kotori's avatar
Otowa Kotori committed
			tformat(damDesc(self, DamageType.ACID, data.power + data.inc_stat), data.dur or 3)
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local pow = data.power
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[damage %d; dur %d; cd %d]]):tformat(damDesc(self, DamageType.ACID, data.power + data.inc_stat), data.dur or 3, data.cooldown)
dg's avatar
dg committed
}

-- Incredibly specific to one resource top, a generalization applying to the other arcane resources is worth considering
-- This serves as one of the primary counters to mana drain effects since it lets you recover from hitting 0
dg's avatar
dg committed
newInscription{
dg's avatar
dg committed
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	use_only_arcane = 1,
	no_break_stealth = true,
	tactical = { MANA = 1 },
	on_pre_use = function(self, t)
		return self:knowTalent(self.T_MANA_POOL) and not self:hasEffect(self.EFF_MANASURGE)
dg's avatar
dg committed
	end,
	on_learn = function(self, t)
		self.mana_regen_on_rest = (self.mana_regen_on_rest or 0) + 0.5
	end,
	on_unlearn = function(self, t)
		self.mana_regen_on_rest = (self.mana_regen_on_rest or 0) - 0.5
	end,
dg's avatar
dg committed
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		self:incMana((data.mana + data.inc_stat) / 20)
dg's avatar
dg committed
		if self.mana_regen > 0 then
			self:setEffect(self.EFF_MANASURGE, data.dur, {power=self.mana_regen * (data.mana + data.inc_stat) / 100})
		else
			if self.mana_regen < 0 then
				game.logPlayer(self, "Your negative mana regeneration rate is unaffected by the rune.")
			else
dg's avatar
dg committed
				game.logPlayer(self, "Your nonexistant mana regeneration rate is unaffected by the rune.")
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local total = (data.mana + data.inc_stat) / 100 * (self.mana_regen or 0) * 10
		return ([[Activate the rune to unleash a manasurge upon yourself, increasing mana regeneration by %d%% for %d turns (%d total) and instantly restoring %d mana.
Otowa Kotori's avatar
Otowa Kotori committed
			Also when resting your mana will regenerate at 0.5 per turn.]]):tformat(data.mana + data.inc_stat, data.dur, total, (data.mana + data.inc_stat) / 20)
dg's avatar
dg committed
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[regen %d%% over %d turns; mana %d; cd %d]]):tformat(data.mana + data.inc_stat, data.dur, (data.mana + data.inc_stat) / 20, data.cooldown)
dg's avatar
dg committed
}
-- Upgrade me
newInscription{
	name = "Rune of the Rift",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	tactical = { DISABLE = 2, ATTACK = { TEMPORAL = 1 } },
	direct_hit = true,
	reflectable = true,
	requires_target = true,
Eric Wykoff's avatar
Eric Wykoff committed
	range = 6,
	target = function(self, t)
		return {type="hit", range=self:getTalentRange(t), talent=t}
	end,
	getDamage = function(self, t) return 150 + self:getWil() * 4 end,
	getDuration = function(self, t) return 4 end,
	action = function(self, t)
		local tg = self:getTalentTarget(t)
		local x, y = self:getTarget(tg)
		if not x or not y then return nil end
		local _ _, x, y = self:canProject(tg, x, y)
		local target = game.level.map(x, y, Map.ACTOR)
		if not target then return end

		if target:attr("timetravel_immune") then
Otowa Kotori's avatar
Otowa Kotori committed
			game.logSeen(target, "%s is immune!", target:getName():capitalize())
Eric Wykoff's avatar
Eric Wykoff committed
			return true
		local hit = self:checkHit(self:combatSpellpower(), target:combatSpellResist() + (target:attr("continuum_destabilization") or 0))
Otowa Kotori's avatar
Otowa Kotori committed
		if not hit then game.logSeen(target, "%s resists!", target:getName():capitalize()) return true end
		self:project(tg, x, y, DamageType.TEMPORAL, self:spellCrit(t.getDamage(self, t)))
		game.level.map:particleEmitter(x, y, 1, "temporal_thrust")
		game:playSoundNear(self, "talents/arcane")
Eric Wykoff's avatar
Eric Wykoff committed
		self:incParadox(-25)
		if target.dead or target.player then return true end
		target:setEffect(target.EFF_CONTINUUM_DESTABILIZATION, 100, {power=self:combatSpellpower(0.3)})
dg's avatar
dg committed
		
	-- Placeholder for the actor
		local oe = game.level.map(x, y, Map.TERRAIN+1)
		if (oe and oe:attr("temporary")) or game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") then game.logPlayer(self, "Something has prevented the timetravel.") return true end
			old_feat = oe, type = "temporal", subtype = "instability",
Otowa Kotori's avatar
Otowa Kotori committed
			name = _t"temporal instability",
			display = '&', color=colors.LIGHT_BLUE,
			temporary = t.getDuration(self, t),
			canAct = false,
			target = target,
			act = function(self)
				self:useEnergy()
				self.temporary = self.temporary - 1
					-- remove ourselves
					if self.old_feat then game.level.map(self.target.x, self.target.y, engine.Map.TERRAIN+1, self.old_feat)
					else game.level.map:remove(self.target.x, self.target.y, engine.Map.TERRAIN+1) end
					game.nicer_tiles:updateAround(game.level, self.target.x, self.target.y)
					game.level:removeEntity(self)
					game.level.map:removeParticleEmitter(self.particles)
					
					-- return the actor and reset their values
					local mx, my = util.findFreeGrid(self.target.x, self.target.y, 20, true, {[engine.Map.ACTOR]=true})
dg's avatar
dg committed
					local old_levelup = self.target.forceLevelup
Eric Wykoff's avatar
Eric Wykoff committed
					local old_check = self.target.check
dg's avatar
dg committed
					self.target.forceLevelup = function() end
Eric Wykoff's avatar
Eric Wykoff committed
					self.target.check = function() end
					game.zone:addEntity(game.level, self.target, "actor", mx, my)
dg's avatar
dg committed
					self.target.forceLevelup = old_levelup
Eric Wykoff's avatar
Eric Wykoff committed
					self.target.check = old_check
			summoner_gain_exp = true, summoner = self,
dg's avatar
dg committed
		
		-- Remove the target
Otowa Kotori's avatar
Otowa Kotori committed
		game.logSeen(target, "%s has moved forward in time!", target:getName():capitalize())
DarkGod's avatar
DarkGod committed
		game.level:removeEntity(target, true)
		
		-- add the time skip object to the map
		local particle = Particles.new("wormhole", 1, {image="shockbolt/terrain/temporal_instability_yellow", speed=1})
		particle.zdepth = 6
		e.particles = game.level.map:addParticleEmitter(particle, x, y)
		game.level.map(x, y, Map.TERRAIN+1, e)
		return true
	end,
	info = function(self, t)
		local damage = t.getDamage(self, t)
		local duration = t.getDuration(self, t)
dg's avatar
dg committed
		return ([[Inflicts %0.2f temporal damage.  If your target survives, it will be sent %d turns into the future.
Eric Wykoff's avatar
Eric Wykoff committed
		It will also lower your paradox by 25 (if you have any).
Otowa Kotori's avatar
Otowa Kotori committed
		Note that messing with the spacetime continuum may have unforeseen consequences.]]):tformat(damDesc(self, DamageType.TEMPORAL, damage), duration)
	end,
	short_info = function(self, t)
Otowa Kotori's avatar
Otowa Kotori committed
		return ("%0.2f temporal damage, removed from time %d turns"):tformat(t.getDamage(self, t), t.getDuration(self, t))
dg's avatar
dg committed

Chris Davidson's avatar
Chris Davidson committed
-- Fix requiring vision
	image = "talents/rune__controlled_phase_door.png",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	is_teleport = true,
	tactical = {  ESCAPE = 1, CLOSEIN = 1 },
	range = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Chris Davidson's avatar
Chris Davidson committed
		return math.floor(data.range + data.inc_stat)
	target = function(self, t) return {type="hit", nolock=true, pass_terrain=false, nowarning=true, range=self:getTalentRange(t), 
		grid_params = {want_range = (not self.ai_target.actor or self.ai_state.tactic == "escape") and 6 or 1	} } end,
	getDur = function(self, t) return 3 end,
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local x, y = self:getTarget(tg)
		if not x then return end
		if not self:hasLOS(x, y) or game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") then return end
		
		game.level.map:particleEmitter(self.x, self.y, 1, "teleport")
		self:teleportRandom(x, y, rad)
		game.level.map:particleEmitter(self.x, self.y, 1, "teleport")

		self:setEffect(self.EFF_OUT_OF_PHASE, data.dur or 3, {
			defense = data.power + data.inc_stat * 3,
			resists = data.power + data.inc_stat * 3,
			effect_reduction = data.power + data.inc_stat * 3,
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local power = data.power + data.inc_stat * 3
		return ([[Activate the rune to teleport up to %d spaces within line of sight.  Afterwards you stay out of phase for %d turns. In this state all new negative status effects duration is reduced by %d%%, your defense is increased by %d and all your resistances by %d%%.]]):
			tformat(t.range(self, t), t.getDur(self, t), power, power, power)
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local power = data.power + data.inc_stat * 3
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[range %d; phase %d; cd %d]]):tformat(self:getTalentRange(t), power, data.cooldown )
-- Invisibility updated to have combat value and more escape potential
	image = "talents/rune__invisibility.png",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	no_energy = true,
	getResistance = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.resist + data.inc_stat * 2
	end,
	getReduction = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.reduction
	end,
	getPower = function(self, t) 
		local data = self:getInscriptionData(t.short_name)
		return math.ceil(data.power + data.inc_stat * 2)
	end,
	getMove = function(self, t) 
		local data = self:getInscriptionData(t.short_name)
		return data.move + data.inc_stat
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		self:setEffect(self.EFF_ETHEREAL, t.getDur(self, t), {power=t.getPower(self, t), reduction=t.getReduction(self, t), resist=t.getResistance(self, t), move=t.getMove(self, t)})
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return ([[Activate the rune to become ethereal for %d turns.
		While ethereal all damage you deal is reduced by %d%%, you gain %d%% all resistance, you move %d%% faster, and you are invisible (power %d).]]):
Otowa Kotori's avatar
Otowa Kotori committed
			tformat(t.getDur(self, t),t.getReduction(self, t) * 100, t.getResistance(self, t), t.getMove(self, t), t.getPower(self, t))
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[power %d; resist %d%%; move %d%%; dur %d; cd %d]]):tformat(t.getPower(self, t), t.getResistance(self, t), t.getMove(self, t), t.getDur(self, t), data.cooldown)
	end,
}

-- Lightning Rune replacement, concept partially kept
-- Numbers on this must be done carefully
newInscription{
	name = "Rune: Stormshield",
	image = "talents/rune__lightning.png",
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	no_energy = true,
	tactical = { DEFEND = 1 },
	getDur = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.dur
	end,
	getThreshold = function(self, t) 
		local data = self:getInscriptionData(t.short_name)
		return math.ceil(data.threshold)
	end,
	getBlocks = function(self, t) 
		local data = self:getInscriptionData(t.short_name)
		return math.ceil(data.blocks + data.inc_stat)
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		self:setEffect(self.EFF_STORMSHIELD, t.getDur(self, t), {threshold=t.getThreshold(self, t), blocks=t.getBlocks(self, t)})
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return ([[Activate the rune to summon a protective storm around you for %d turns.
			While active the storm will completely block all damage over %d up to %d times.]])
Otowa Kotori's avatar
Otowa Kotori committed
				:tformat(t.getDur(self, t), t.getThreshold(self, t), t.getBlocks(self, t) )
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[threshold %d; blocks %d; dur %d; cd %d]]):tformat(t.getThreshold(self, t), t.getBlocks(self, t), t.getDur(self, t), data.cooldown  )

-- Fixedart generated with a random ward set
newInscription{
	name = "Rune: Prismatic",
	image = "talents/ward.png", -- re-used icon
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	no_break_stealth = true,
	tactical = { DEFEND = 3,},
	getDur = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.dur
	end,
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Chris Davidson's avatar
Chris Davidson committed
		self:setEffect(self.EFF_PRISMATIC_SHIELD, t.getDur(self, t), {wards = table.clone(data.wards)})
		return true
	end,
	info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		local str = ""
		for k,v in pairs(data.wards) do
Otowa Kotori's avatar
Otowa Kotori committed
			str = str .. ", " .. v .. " " .. _t(k:lower())
		end
		str = string.sub(str, 2)
Chris Davidson's avatar
Chris Davidson committed
		return ([[Activate the rune to create a shield for %d turns blocking several instances of damage of the following types:%s]]) -- color me
Otowa Kotori's avatar
Otowa Kotori committed
				:tformat(t.getDur(self, t), str)
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		local str = table.concat(table.ts(table.lower(table.keys(data.wards))), ", ")
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[%d turns; %s]]):tformat(t.getDur(self, t), str:lower() )
-- Fixedart
newInscription{
	name = "Rune: Mirror Image",
	type = {"inscriptions/runes", 1},
	image = "talents/phase_shift.png",  -- re-used icon
	points = 1,
	is_spell = true,
	no_break_stealth = true,
Chris Davidson's avatar
Chris Davidson committed
	tactical = { DEFEND = 3,},
	getDur = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.dur
	end,
	getInheritance = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.inheritance
	end,
	getInheritedResist = function(self, t)
		local res = {}
		for k,v in pairs(self.resists) do
			res[k] = (t.getInheritance(self, t) * (self.resists[k]) or 0)
		end
		return res
	end,
	action = function(self, t)
		if not self:canBe("summon") then game.logPlayer(self, "You cannot summon; you are suppressed!") return end

		-- Find all actors in radius 10 and add them to a table
		local tg = {type="ball", radius=self.sight}
		local grids = self:project(tg, self.x, self.y, function() end)
		local tgts = {}
		for x, ys in pairs(grids) do for y, _ in pairs(ys) do
			local target = game.level.map(x, y, Map.ACTOR)
			if target and self:reactionToward(target) < 0 then tgts[#tgts+1] = target end
		end end

		for _ = 1,3 do
			local target = rng.tableRemove(tgts)
			if target then
				local tx, ty = util.findFreeGrid(target.x, target.y, 10, true, {[Map.ACTOR]=true})
				if tx then
					local Talents = require "engine.interface.ActorTalents"
					local NPC = require "mod.class.NPC"
					local caster = self
					local image = NPC.new{
						name = _t"Mirror Image",
						type = "image", subtype = "image",
						ai = "summoned", ai_real = nil, ai_state = { talent_in=1, }, ai_target = {actor=nil},
						desc = _t"A blurred image.",
						image = caster.image,
						add_mos = table.clone(caster.add_mos, true),
						shader = "shadow_simulacrum", shader_args = { color = {0.0, 0.4, 0.8}, base = 0.6, time_factor = 1500 },
						exp_worth=0,
						max_life = caster.max_life,
						life = caster.max_life, -- We don't want to make this only useful before you take damage
						combat_armor_hardiness = caster:combatArmorHardiness(),
						combat_def = caster:combatDefense(),
						combat_armor = caster:combatArmor(),
						size_category = caster.size_category,
						resists = t.getInheritedResist(self, t),
						rank = 1,
						life_rating = 0,
						cant_be_moved = 1,
						never_move = 1,
						never_anger = true,
						resolvers.talents{
							[Talents.T_TAUNT]=1, -- Add the talent so the player can see it even though we cast it manually
						},
						on_act = function(self) -- avoid any interaction with .. uh, anything
							self:forceUseTalent(self.T_TAUNT, {ignore_cd=true, no_talent_fail = true})
						end,
						faction = caster.faction,
						summoner = caster,
						summon_time=t.getDur(self, t),
						no_breath = 1,
						remove_from_party_on_death = true,
					}

					image:resolve()
					game.zone:addEntity(game.level, image, "actor", tx, ty)
					if game.party:hasMember(self) then
						game.party:addMember(image, {
							control=false,
							type="summon",
Otowa Kotori's avatar
Otowa Kotori committed
							title=_t"Summon",

					image:forceUseTalent(image.T_TAUNT, {ignore_cd=true, no_talent_fail = true})

		return true
	end,
	info = function(self, t)
		return ([[Activate the rune to create up to 3 images of yourself that taunt nearby enemies each turn and immediately after being summoned.
			Only one image can be created per enemy in radius 10 with the first being created near the closest enemy.
			Images inherit all of your life, resistance, armor, defense, and armor hardiness.]])
Otowa Kotori's avatar
Otowa Kotori committed
				:tformat(t.getInheritance(self, t)*100 )
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[dur %d; cd %d]]):tformat(t.getDur(self, t), data.cooldown)
	end
}

-- Counter to mass multitype debuff spam
-- This is the counterpart to Wild but scales differently, acknowledging the fact that triple type cleanse becomes better as the game progresses
newInscription{
	name = "Rune: Shatter Afflictions",
	image = "talents/warp_mine_away.png", -- re-used icon
	type = {"inscriptions/runes", 1},
	points = 1,
	tactical = { CURE = function(self, t, target)
		local types = 0
		types = types + #self:effectsFilter({status="detrimental", type="physical"}, 1)
		types = types + #self:effectsFilter({status="detrimental", type="magical"}, 1)
		types = types + #self:effectsFilter({status="detrimental", type="mental"}, 1)
		return types
	end
	},
	is_spell = true,
	no_energy = true,
	getShield = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		return data.shield + data.inc_stat
	end,
Otowa Kotori's avatar
Otowa Kotori committed
	getDuration = function(self, t)
		return 3
	end,
	on_pre_use = function(self, t)
		if next(self:effectsFilter({type="physical", status="detrimental"}, 1)) then return true end
		if next(self:effectsFilter({type="magical", status="detrimental"}, 1)) then return true end
		if next(self:effectsFilter({type="mental", status="detrimental"}, 1)) then return true end
		if next(self:effectsFilter({subtype={["cross tier"] = true}, status="detrimental"}, 3)) then return true end
		return false
	end,
	action = function(self, t)
		local data = self:getInscriptionData(t.short_name)
		
		local crosstiers = self:removeEffectsFilter(self, {subtype={["cross tier"] = true}, status="detrimental"}, 3)
		local cleansed = 0
		cleansed = cleansed + self:removeEffectsFilter(self, {type="physical", status="detrimental"}, 1)
		cleansed = cleansed + self:removeEffectsFilter(self, {type="magical", status="detrimental"}, 1)
		cleansed = cleansed + self:removeEffectsFilter(self, {type="mental", status="detrimental"}, 1)
		if crosstiers == 0 and cleansed == 0 then return nil end

		if cleansed > 0 then
Otowa Kotori's avatar
Otowa Kotori committed
			self:setEffect(self.EFF_DAMAGE_SHIELD, t.getDuration(self, t), {power=(data.shield + data.inc_stat) * cleansed})
		else
			game:onTickEnd(function() self:alterTalentCoolingdown(t.id, -math.floor((self.talents_cd[t.id] or 0) * 0.75)) end)
		end

		return true
	end,
	info = function(self, t)
		return ([[Activate the rune to instantly dissipate the energy of your ailments, cleansing all cross tier effects and 1 physical, mental, and magical effect.
Otowa Kotori's avatar
Otowa Kotori committed
		You use the dissipated energy to create a shield lasting %d turns and blocking %d damage per debuff cleansed (not counting cross-tier ones).
		If there were only cross-tier effects to cleanse, no shield is created and the rune goes on a 75%% reduced cooldown.]])
		:tformat(self:getShieldDuration(t.getDuration(self, t)), self:getShieldAmount(t.getShield(self, t)))
	end,
	short_info = function(self, t)
		local data = self:getInscriptionData(t.short_name)
Otowa Kotori's avatar
Otowa Kotori committed
		return ([[absorb %d; cd %d]]):tformat(self:getShieldAmount(t.getShield(self, t)), data.cooldown)
	end,
}

newInscription{
	name = "Rune: Dissipation",
	image = "talents/disperse_magic.png",  -- re-used icon
	type = {"inscriptions/runes", 1},
	points = 1,
	is_spell = true,
	range = 10,
	direct_hit = true,
	target = function(self, t) return {default_target=self, type="hit", nowarning=true, range=self:getTalentRange(t)} end,
	tactical = {
		DISABLE = function(self, t, aitarget)
			local nb = 0
			for tid, act in pairs(aitarget.sustain_talents) do
				if act then
					local talent = aitarget:getTalentFromId(tid)
					if talent.is_spell then nb = nb + 1 end
				end
			end