Skip to content
Snippets Groups Projects
arcane.lua 14.9 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


newTalent{
	name = "Manathrust",
	type = {"spell/arcane", 1},
	require = spells_req1,
	random_ego = "attack",
dg's avatar
dg committed
	mana = 10,
dg's avatar
dg committed
	cooldown = 3,
dg's avatar
dg committed
	use_only_arcane = 1,
	tactical = { ATTACK = { ARCANE = 2 } },
dg's avatar
dg committed
	direct_hit = function(self, t) if self:getTalentLevel(t) >= 3 then return true else return false end end,
dg's avatar
dg committed
	reflectable = true,
	requires_target = true,
		if thaumaturgyCheck(self) then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end
		local tg = {type="bolt", range=self:getTalentRange(t), talent=t, display={particle="bolt_arcane", trail="arcanetrail"}}
dg's avatar
dg committed
		if self:getTalentLevel(t) >= 3 then tg.type = "beam" end
		return tg
	end,
	getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 230) end,
	action = function(self, t)
		local tg = self:getTalentTarget(t)
		local x, y = self:getTarget(tg)
dg's avatar
dg committed
		if not x or not y then return nil end
DarkGod's avatar
DarkGod committed
		local dam = thaumaturgyBeamDamage(self, self:spellCrit(t.getDamage(self, t)))
DarkGod's avatar
DarkGod committed
		if tg.type == "beam" or tg.type == "widebeam" then
DarkGod's avatar
DarkGod committed
			self:project(tg, x, y, DamageType.ARCANE, dam, nil)
DarkGod's avatar
DarkGod committed
			local _ _, x, y = self:canProject(tg, x, y)
DarkGod's avatar
DarkGod committed
				game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(x-self.x), math.abs(y-self.y)), "mana_beam_wide", {tx=x-self.x, ty=y-self.y})
			else
				game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(x-self.x), math.abs(y-self.y)), "mana_beam", {tx=x-self.x, ty=y-self.y})
			end
dg's avatar
dg committed
		else
DarkGod's avatar
DarkGod committed
			self:projectile(tg, x, y, DamageType.ARCANE, dam, {type="manathrust"})
dg's avatar
dg committed
		end
dg's avatar
dg committed
		game:playSoundNear(self, "talents/arcane")
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
dg's avatar
dg committed
		local damage = t.getDamage(self, t)
dg's avatar
dg committed
		return ([[Conjures up mana into a powerful bolt doing %0.2f arcane damage.
dg's avatar
dg committed
		At level 3, it becomes a beam.
dg's avatar
dg committed
		The damage will increase with your Spellpower.]]):
Otowa Kotori's avatar
Otowa Kotori committed
		tformat(damDesc(self, DamageType.ARCANE, damage))
dg's avatar
dg committed
	end,
}
dg's avatar
dg committed

newTalent{
	name = "Arcane Power",
	type = {"spell/arcane", 2},
	mode = "sustained",
	require = spells_req2,
	sustain_mana = 25,
	points = 5,
	cooldown = 30,
	tactical = { BUFF = 2 },
	use_only_arcane = 1,
	getSpellpowerIncrease = function(self, t) return self:combatTalentScale(t, 5, 20, 0.75) end,
	getArcaneResist = function(self, t) return 5 + self:combatTalentSpellDamage(t, 10, 500) / 18 end,
	activate = function(self, t)
		game:playSoundNear(self, "talents/arcane")
		return {
			res = self:addTemporaryValue("resists", {[DamageType.ARCANE] = t.getArcaneResist(self, t)}),
			display_resist = t.getArcaneResist(self, t),
			power = self:addTemporaryValue("combat_spellpower", t.getSpellpowerIncrease(self, t)),
			particle = self:addParticles(Particles.new("arcane_power", 1)),
		}
	end,
	deactivate = function(self, t, p)
		self:removeParticles(p.particle)
		self:removeTemporaryValue("combat_spellpower", p.power)
		self:removeTemporaryValue("resists", p.res)
		return true
	end,
	info = function(self, t)
		local resist = self.sustain_talents[t.id] and self.sustain_talents[t.id].display_resist or t.getArcaneResist(self, t)
		return ([[Your mastery of magic allows you to enter a state of deep concentration, increasing your Spellpower by %d and arcane resistance by %d%%.]]):
Otowa Kotori's avatar
Otowa Kotori committed
		tformat(t.getSpellpowerIncrease(self, t), resist)
dg's avatar
dg committed
newTalent{
dg's avatar
dg committed
	name = "Arcane Vortex",
	type = {"spell/arcane", 3},
	require = spells_req3,
Chris Davidson's avatar
Chris Davidson committed
	mana = 20,
dg's avatar
dg committed
	cooldown = 12,
dg's avatar
dg committed
	use_only_arcane = 1,
dg's avatar
dg committed
	range = 10,
	direct_hit = true,
	reflectable = true,
	requires_target = true,
	tactical = { ATTACK = { ARCANE = 2 } },
	getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 340) / 6 end,
dg's avatar
dg committed
	action = function(self, t)
dg's avatar
dg committed
		local tg = {type="hit", range=self:getTalentRange(t), talent=t}
		local tx, ty, target = self:getTarget(tg)
		if not tx or not ty then return nil end
		local _ _, tx, ty = self:canProject(tg, tx, ty)
		target = game.level.map(tx, ty, Map.ACTOR)
		if not target then return nil end

		self:callTalent(self.T_ENERGY_ALTERATION, "forceActivate", DamageType.ARCANE)
Chris Davidson's avatar
Chris Davidson committed
		target:setEffect(target.EFF_ARCANE_VORTEX, 6, {src=self, dam=self:spellCrit(t.getDamage(self, t))})
dg's avatar
dg committed
		game:playSoundNear(self, "talents/arcane")
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
dg's avatar
dg committed
		local dam = t.getDamage(self, t)
dg's avatar
dg committed
		return ([[Creates a vortex of arcane energies on the target for 6 turns. Each turn the vortex will look for another foe in sight and fire a manathrust doing %0.2f arcane damage to all foes in line.
		If no foes are found, the target will take 50%% more arcane damage.
dg's avatar
dg committed
		If the target dies, the vortex explodes, releasing all remaining damage in a radius 2 ball of arcane force.
dg's avatar
dg committed
		The damage will increase with your Spellpower.]]):
Otowa Kotori's avatar
Otowa Kotori committed
		tformat(damDesc(self, DamageType.ARCANE, dam))
dg's avatar
dg committed
	end,
}
dg's avatar
dg committed

Chris Davidson's avatar
Chris Davidson committed
-- Mana gain on deactivation is mostly to preserve this talent's role as a counter to mana drain
dg's avatar
dg committed
newTalent{
	name = "Disruption Shield",
	type = {"spell/arcane",4},
	require = spells_req4, no_sustain_autoreset = true,
dg's avatar
dg committed
	mode = "sustained",
dg's avatar
dg committed
	use_only_arcane = 1,
dg's avatar
dg committed
	tactical = { MANA = 3, DEFEND = 2, },
	radius = function(self, t) return self:hasEffect(self.EFF_AETHER_AVATAR) and 10 or 5 end,
Otowa Kotori's avatar
Otowa Kotori committed
	getMaxAbsorb = function(self, t) return self:getShieldAmount(self:combatTalentSpellDamage(t, 50, 450)) end,
Lisa Greene's avatar
Lisa Greene committed
	getManaRatio = function(self, t) return self:combatTalentLimit(t, 0.2, 0.95, 0.35) end,
dg's avatar
dg committed
	-- Note: effects handled in mod.class.Actor:onTakeHit function
	getMaxDamageLimit = function(self, t) return self:combatTalentLimit(t, 1200, 400, 1000) end,
dg's avatar
dg committed
	getMaxDamage = function(self, t) -- Compute damage limit
		local max_dam = self.max_mana
		for i, k in pairs(self.sustain_talents) do -- Add up sustain costs to get total mana pool size
			max_dam = max_dam + (tonumber(self.talents_def[i].sustain_mana) or 0)
		end
		max_dam = math.min(max_dam, t.getMaxDamageLimit(self, t))
dg's avatar
dg committed
		return max_dam * 2 -- Maximum damage is 2x total mana pool
	end,
dg's avatar
dg committed
	explode = function(self, t, dam)
Otowa Kotori's avatar
Otowa Kotori committed
		game.logSeen(self, "#VIOLET#%s's disruption shield collapses and then explodes in a powerful manastorm!", self:getName():capitalize())
dg's avatar
dg committed
		dam = math.min(dam, t.getMaxDamage(self, t)) -- Damage cap
dg's avatar
dg committed
		-- Add a lasting map effect
		local radius = self:getTalentRadius(t)
dg's avatar
dg committed
		game.level.map:addEffect(self,
			self.x, self.y, 5,
			DamageType.ARCANE, self:spellCrit(dam / 5),
dg's avatar
dg committed
			5, nil,
			{type="arcanestorm", args={radius=radius}, only_one=true},
dg's avatar
dg committed
			function(e) e.x = e.src.x e.y = e.src.y return true end,
dg's avatar
dg committed
		)
	end,
dg's avatar
dg committed
	damage_feedback = function(self, t, p, src)
		if p.particle and p.particle._shader and p.particle._shader.shad and src and src.x and src.y then
			local r = -rng.float(0.2, 0.4)
			local a = math.atan2(src.y - self.y, src.x - self.x)
			p.particle._shader:setUniform("impact", {math.cos(a) * r, math.sin(a) * r})
			p.particle._shader:setUniform("impact_tick", core.game.getTime())
		end
	end,
	iconOverlay = function(self, t, p)
		local val = self.disruption_shield_storage or 0
		if val <= 0 then return "" end
		local fnt = "buff_font_small"
		if val >= 1000 then fnt = "buff_font_smaller" end
		return tostring(math.ceil(val)), fnt
	end,
	callbackOnRest = function(self, t)
		self.disruption_shield_power = self.disruption_shield_power or 0
		if self.disruption_shield_power < t.getMaxAbsorb(self, t) then return true end
	end,
	callbackOnAct = function(self, t, state)
		if self.in_combat then return end
		self.disruption_shield_power = self.disruption_shield_power or 0
		self.disruption_shield_storage = self.disruption_shield_storage or 0
		self.disruption_shield_storage = self.disruption_shield_storage / 3
		if self.disruption_shield_storage < 100 then self.disruption_shield_storage = 0 end
		local max = t.getMaxAbsorb(self, t)
		self.disruption_shield_power = math.min(self.disruption_shield_power + max / 10, max)
DarkGod's avatar
DarkGod committed

		local p = self:isTalentActive(t.id)
		if p and p.was_depleted and core.shader.active(4) then
			p.was_depleted = false
			self:removeParticles(p.particle)
			p.particle = self:addParticles(Particles.new("shader_shield", 1, {size_factor=1.4, img="runicshield"}, {type="runicshield", shieldIntensity=0.1, ellipsoidalFactor=1, scrollingSpeed=-1, time_factor=12000, bubbleColor={0.8, 0.1, 1.0, 0.8}, auraColor={0.85, 0.3, 1.0, 0.8}}))
		end		
	doLostMana = function(self, t, mana)
		if (self:getMana() - mana) / self:getMaxMana() < 0.5 then
			self:forceUseTalent(self.T_DISRUPTION_SHIELD, {ignore_energy=true})
		end
	end,
	callbackOnHit = function(self, t, cb, src, dt)
		local p = self:isTalentActive(t.id)
		if not p then return end
		if self:getMana() / self:getMaxMana() < 0.5 then
			self:forceUseTalent(self.T_DISRUPTION_SHIELD, {ignore_energy=true})
			return
		end
		-- if self:reactionToward(src) > 0 then return end
		self.disruption_shield_power = self.disruption_shield_power or 0
		self.disruption_shield_storage = self.disruption_shield_storage or 0
Otowa Kotori's avatar
Otowa Kotori committed
		local absorbed = 0
		if cb.value <= self.disruption_shield_power then
			self.disruption_shield_power = self.disruption_shield_power - cb.value
Otowa Kotori's avatar
Otowa Kotori committed
			absorbed = cb.value
Otowa Kotori's avatar
Otowa Kotori committed
			game:delayedLogDamage(src, self, 0, ("#SLATE#(%d absorbed)#LAST#"):tformat(absorbed), false)
			return true
		else
			cb.value = cb.value - self.disruption_shield_power
Otowa Kotori's avatar
Otowa Kotori committed
			absorbed = self.disruption_shield_power
			self.disruption_shield_power = 0
		if not p.was_depleted then
			p.was_depleted = true
			if core.shader.active(4) then
				self:removeParticles(p.particle)
				p.particle = self:addParticles(Particles.new("shader_shield", 1, {size_factor=1.8, img="shield6"}, {type="runicshield", shieldIntensity=0.1, ellipsoidalFactor=1, scrollingSpeed=1, time_factor=2000, bubbleColor={0.8, 0.1, 1.0, 0.8}, auraWidth=0.2, auraColor={0.85, 0.3, 1.0, 0.8}}))
			end
		end

		local do_explode = false
		local ratio = t.getManaRatio(self, t)
		local mana_usage = cb.value * ratio
		local store = cb.value

Otowa Kotori's avatar
Otowa Kotori committed
		if self.disruption_shield_storage + store >= t.getMaxDamage(self, t) then
			do_explode = true
			store = t.getMaxDamage(self, t) - self.disruption_shield_storage
			mana_usage = store * ratio
		end
DarkGod's avatar
DarkGod committed
		if (self:getMana() - mana_usage) / self:getMaxMana() < 0.5 then
			do_explode = true
			local mana_limit = self:getMaxMana() * 0.3
			mana_usage = self:getMana() - mana_limit
			store = mana_usage / ratio
		end
Otowa Kotori's avatar
Otowa Kotori committed
		cb.value = cb.value - store
		absorbed = absorbed + store
		self:incMana(-mana_usage)
		self.disruption_shield_storage = math.min(self.disruption_shield_storage + store, t.getMaxDamage(self, t))
Otowa Kotori's avatar
Otowa Kotori committed
		game:delayedLogDamage(src, self, 0, ("#SLATE#(%d absorbed)#LAST#"):tformat(absorbed), false)
Otowa Kotori's avatar
Otowa Kotori committed
		game:delayedLogDamage(src, self, 0, ("#PURPLE#(%d mana)#LAST#"):tformat(store), false)
			-- Deactivate without losing energy
			self:forceUseTalent(self.T_DISRUPTION_SHIELD, {ignore_energy=true})
		end
		return true
	end,
	doAegis = function(self, t, val)
		local energy = self.disruption_shield_storage or 0
		local max = t.getMaxAbsorb(self, t)
		local eneed = (max - self.disruption_shield_power) * 2

		if eneed < energy then
			self.disruption_shield_power = max
			self:incMana((energy - eneed) * val / 100)
Otowa Kotori's avatar
Otowa Kotori committed
			game.logSeen(self, "%s restores Disruption Shield (+%d) and gains %d mana with Aegis!", self:getName(), eneed/2, (energy - eneed) * val / 100)
		else
			self.disruption_shield_power = self.disruption_shield_power + energy / 2
Otowa Kotori's avatar
Otowa Kotori committed
			game.logSeen(self, "%s restores Disruption Shield (+%d) with Aegis!", self:getName(), energy / 2)

		local p = self:isTalentActive(t.id)
		if not p then return end
		p.was_depleted = false
		if core.shader.active(4) then
			self:removeParticles(p.particle)
			p.particle = self:addParticles(Particles.new("shader_shield", 1, {size_factor=1.4, img="runicshield"}, {type="runicshield", shieldIntensity=0.1, ellipsoidalFactor=1, scrollingSpeed=-1, time_factor=12000, bubbleColor={0.8, 0.1, 1.0, 0.8}, auraColor={0.85, 0.3, 1.0, 0.8}}))
		end		
dg's avatar
dg committed
	activate = function(self, t)
		self.disruption_shield_storage = 0
		self.disruption_shield_power = t.getMaxAbsorb(self, t)
dg's avatar
dg committed
		game:playSoundNear(self, "talents/arcane")
dg's avatar
dg committed

		local particle
dg's avatar
dg committed
		if core.shader.active(4) then
			particle = self:addParticles(Particles.new("shader_shield", 1, {size_factor=1.4, img="runicshield"}, {type="runicshield", shieldIntensity=0.1, ellipsoidalFactor=1, scrollingSpeed=-1, time_factor=12000, bubbleColor={0.8, 0.1, 1.0, 0.8}, auraColor={0.85, 0.3, 1.0, 0.8}}))
dg's avatar
dg committed
		else
			particle = self:addParticles(Particles.new("disruption_shield", 1))
		end

dg's avatar
dg committed
		return {
dg's avatar
dg committed
			particle = particle,
dg's avatar
dg committed
		}
	end,
	deactivate = function(self, t, p)
dg's avatar
dg committed
		self:removeParticles(p.particle)
Chris Davidson's avatar
Chris Davidson committed
		if self:attr("save_cleanup") then return true end

DarkGod's avatar
DarkGod committed
		if self.disruption_shield_storage and self.disruption_shield_storage > 0 then t.explode(self, t, self.disruption_shield_storage) end
		self.disruption_shield_storage = nil
		self.disruption_shield_power = nil
dg's avatar
dg committed
		return true
	end,
	info = function(self, t)
		return ([[Surround yourself with arcane forces, disrupting any attempts to harm you by creating a shield of pure aether which can absorb %d damage.
DarkGod's avatar
DarkGod committed
		In combat, the mental focus required to maintain and monitor the shield is too much and you let it run on its own. In this state once the shield power is depleted it will start using your mana to absorb hits, at a ratio of %0.2f mana per damage.
		Whenever mana is used by the shield it stores a remnant of this energy (up to %d max). When the shield is deactivated any stored energy is released in a radius %d arcane storm that lasts 5 turns, dealing 20%% of the total stored damage each turn.
		Outside of combat the shield regenerates 10%% of its power each turn and stored energy quickly dissipates.
DarkGod's avatar
DarkGod committed
		Dropping below 50%% mana or reaching max energy storage will automatically deactivate this talent.
		The shield power improves with your Spellpower.
		The maximum energy storage is based on your total mana (ignoring sustained spells), with a limit at %d effective mana.

		Current shield power: %d
		Current stored energy: %d]]):
Otowa Kotori's avatar
Otowa Kotori committed
		tformat(t.getMaxAbsorb(self, t), t.getManaRatio(self, t), t.getMaxDamage(self, t), self:getTalentRadius(t), t.getMaxDamageLimit(self, t), self.disruption_shield_power or 0, self.disruption_shield_storage or 0)
dg's avatar
dg committed
	end,
}