-- ToME - Tales of Maj'Eyal -- Copyright (C) 2009 - 2015 Nicolas Casalini -- -- 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 -- EDGE TODO: Particles, Timed Effect Particles, Mine Tiles local Trap = require "mod.class.Trap" makeWarpMine = function(self, t, x, y, type) -- Mine values local dam = self:spellCrit(self:callTalent(self.T_WARP_MINES, "getDamage")) local duration = self:callTalent(self.T_WARP_MINES, "getDuration") local detect = math.floor(self:callTalent(self.T_WARP_MINES, "trapPower") * 0.8) local disarm = math.floor(self:callTalent(self.T_WARP_MINES, "trapPower")) local power = getParadoxSpellpower(self, t) local dest_power = getParadoxSpellpower(self, t, 0.3) -- Our Mines local mine = Trap.new{ name = ("warp mine: %s"):format(type), type = "temporal", id_by_type=true, unided_name = "trap", display = '^', color=colors.BLUE, image = ("trap/chronomine_%s_0%d.png"):format(type == "toward" and "blue" or "red", rng.avg(1, 4, 3)), shader = "shadow_simulacrum", shader_args = { color = {0.2, 0.2, 0.2}, base = 0.8, time_factor = 1500 }, dam = dam, talent=t, power = power, dest_power = dest_power, temporary = duration, x = x, y = y, type = type, summoner = self, summoner_gain_exp = true, disarm_power = disarm, detect_power = detect, canTrigger = function(self, x, y, who) if who:reactionToward(self.summoner) < 0 then return mod.class.Trap.canTrigger(self, x, y, who) end return false end, triggered = function(self, x, y, who) -- Project our damage self.summoner:project({type="hit",x=x,y=y, talent=self.talent}, x, y, engine.DamageType.WARP, self.dam) -- Teleport? if not who.dead then -- Does our teleport hit? local hit = self.summoner:checkHit(self.power, who:combatSpellResist() + (who:attr("continuum_destabilization") or 0)) and who:canBe("teleport") if hit then game.level.map:particleEmitter(who.x, who.y, 1, "temporal_teleport") local teleport_done = false if self.type == "toward" then -- since we're using a precise teleport we'll look for a free grid first local tx, ty = util.findFreeGrid(self.summoner.x, self.summoner.y, 5, true, {[engine.Map.ACTOR]=true}) if tx and ty then game.level.map:particleEmitter(who.x, who.y, 1, "temporal_teleport") if not who:teleportRandom(self.summoner.x, self.summoner.y, 1, 0) then game.logSeen(self, "The teleport fizzles!") else teleport_done = true end end elseif self.type == "away" then game.level.map:particleEmitter(who.x, who.y, 1, "temporal_teleport") if not who:teleportRandom(self.summoner.x, self.summoner.y, 10, 5) then game.logSeen(self, "The teleport fizzles!") else teleport_done = true end end -- Destabailize? if teleport_done then who:setEffect(who.EFF_CONTINUUM_DESTABILIZATION, 100, {power=self.dest_power}) game.level.map:particleEmitter(who.x, who.y, 1, "temporal_teleport") game:playSoundNear(self, "talents/teleport") end else game.logSeen(who, "%s resists the teleport!", who.name:capitalize()) end end return true, true end, canAct = false, energy = {value=0}, act = function(self) self:useEnergy() self.temporary = self.temporary - 1 if self.temporary <= 0 then if game.level.map(self.x, self.y, engine.Map.TRAP) == self then game.level.map:remove(self.x, self.y, engine.Map.TRAP) end game.level:removeEntity(self) end end, } return mine end newTalent{ name = "Warp Mines", type = {"chronomancy/spacetime-folding", 1}, points = 5, mode = "passive", require = chrono_req1, getRange = function(self, t) return math.floor(self:combatTalentScale(t, 5, 9, 0.5, 0, 1)) end, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 200, getParadoxSpellpower(self, t)) end, getDuration = function(self, t) return getExtensionModifier(self, t, math.floor(self:combatTalentScale(t, 6, 10))) end, -- Duration of mines trapPower = function(self,t) return math.max(1,self:combatScale(self:getTalentLevel(t) * self:getMag(15, true), 0, 0, 75, 75)) end, -- Used to determine detection and disarm power, about 75 at level 50 on_learn = function(self, t) local lev = self:getTalentLevelRaw(t) if lev == 1 then self:learnTalent(self.T_WARP_MINE_TOWARD, true, nil, {no_unlearn=true}) self:learnTalent(self.T_WARP_MINE_AWAY, true, nil, {no_unlearn=true}) end end, on_unlearn = function(self, t) local lev = self:getTalentLevelRaw(t) if lev == 0 then self:unlearnTalent(self.T_WARP_MINE_TOWARD) self:unlearnTalent(self.T_WARP_MINE_AWAY) end end, info = function(self, t) local range = t.getRange(self, t) local damage = t.getDamage(self, t)/2 local detect = t.trapPower(self,t)*0.8 local disarm = t.trapPower(self,t) local duration = t.getDuration(self, t) return ([[Learn to lay Warp Mines in a radius of 1. Warp Mines teleport targets that trigger them either toward you or away from you depending on the type of mine used and inflict %0.2f physical and %0.2f temporal (warp) damage. The mines are hidden traps (%d detection and %d disarm power based on your Magic), last for %d turns, and share a ten turn cooldown. The damage caused by your Warp Mines will improve with your Spellpower. Investing in this talent improves the range of all Spacetime Folding talents. Current Range: %d]]): format(damDesc(self, DamageType.PHYSICAL, damage), damDesc(self, DamageType.TEMPORAL, damage), detect, disarm, duration, range) --I5 end, } newTalent{ name = "Warp Mine Toward", type = {"chronomancy/other", 1}, points = 1, cooldown = 10, paradox = function (self, t) return getParadoxCost(self, t, 10) end, tactical = { ATTACKAREA = { TEMPORAL = 1, PHYSICAL = 1 }, CLOSEIN = 2 }, requires_target = true, range = function(self, t) return self:callTalent(self.T_WARP_MINES, "getRange") or 5 end, no_unlearn_last = true, target = function(self, t) return {type="ball", nowarning=true, range=self:getTalentRange(t), radius=1, nolock=true, talent=t} end, action = function(self, t) local tg = self:getTalentTarget(t) local tx, ty = self:getTarget(tg) if not tx or not ty then return nil end local __, tx, ty = self:canProject(tg, tx, ty) -- Lay the mines in a ball self:project(tg, tx, ty, function(px, py) local target_trap = game.level.map(px, py, Map.TRAP) if target_trap then return end if game.level.map:checkEntity(px, py, Map.TERRAIN, "block_move") then return end -- Make our mine local trap = makeWarpMine(self, t, px, py, "toward") -- Add the mine game.level:addEntity(trap) trap:identify(true) trap:setKnown(self, true) game.zone:addEntity(game.level, trap, "trap", px, py) end) game:playSoundNear(self, "talents/warp") self:startTalentCooldown(self.T_WARP_MINE_AWAY) return true end, info = function(self, t) local damage = self:callTalent(self.T_WARP_MINES, "getDamage")/2 local duration = self:callTalent(self.T_WARP_MINES, "getDuration") local detect = self:callTalent(self.T_WARP_MINES, "trapPower") * 0.8 local disarm = self:callTalent(self.T_WARP_MINES, "trapPower") return ([[Lay Warp Mines in a radius of 1 that teleport enemies to you and inflict %0.2f physical and %0.2f temporal (warp) damage. The mines are hidden traps (%d detection and %d disarm power based on your Magic) and last for %d turns. The damage caused by your Warp Mines will improve with your Spellpower. Using this talent will trigger the cooldown on Warp Mine Away.]]): format(damDesc(self, DamageType.PHYSICAL, damage), damDesc(self, DamageType.TEMPORAL, damage), detect, disarm, duration) end, } newTalent{ name = "Warp Mine Away", type = {"chronomancy/other", 1}, points = 1, cooldown = 10, paradox = function (self, t) return getParadoxCost(self, t, 10) end, tactical = { ATTACKAREA = { TEMPORAL = 1, PHYSICAL = 1 }, ESCAPE = 2 }, requires_target = true, range = function(self, t) return self:callTalent(self.T_WARP_MINES, "getRange") or 5 end, no_unlearn_last = true, target = function(self, t) return {type="ball", nowarning=true, range=self:getTalentRange(t), radius=1, nolock=true, talent=t} end, action = function(self, t) local tg = self:getTalentTarget(t) local tx, ty = self:getTarget(tg) if not tx or not ty then return nil end local _ _, tx, ty = self:canProject(tg, tx, ty) -- Lay the mines in a ball self:project(tg, tx, ty, function(px, py) local target_trap = game.level.map(px, py, Map.TRAP) if target_trap then return end if game.level.map:checkEntity(px, py, Map.TERRAIN, "block_move") then return end -- Make our mine local trap = makeWarpMine(self, t, px, py, "away") -- Add the mine game.level:addEntity(trap) trap:identify(true) trap:setKnown(self, true) game.zone:addEntity(game.level, trap, "trap", px, py) end) game:playSoundNear(self, "talents/warp") self:startTalentCooldown(self.T_WARP_MINE_TOWARD) return true end, info = function(self, t) local damage = self:callTalent(self.T_WARP_MINES, "getDamage")/2 local duration = self:callTalent(self.T_WARP_MINES, "getDuration") local detect = self:callTalent(self.T_WARP_MINES, "trapPower") * 0.8 local disarm = self:callTalent(self.T_WARP_MINES, "trapPower") return ([[Lay Warp Mines in a radius of 1 that teleport enemies away from you and inflict %0.2f physical and %0.2f temporal (warp) damage. The mines are hidden traps (%d detection and %d disarm power based on your Magic) and last for %d turns. The damage caused by your Warp Mines will improve with your Spellpower. Using this talent will trigger the cooldown on Warp Mine Toward.]]): format(damDesc(self, DamageType.PHYSICAL, damage), damDesc(self, DamageType.TEMPORAL, damage), detect, disarm, duration) end, } newTalent{ name = "Spatial Tether", type = {"chronomancy/spacetime-folding", 2}, require = chrono_req2, points = 5, paradox = function (self, t) return getParadoxCost(self, t, 12) end, cooldown = 8, tactical = { { ATTACKAREA = { TEMPORAL = 1, PHYSICAL = 1 }, DISABLE = 2 }, range = function(self, t) return self:callTalent(self.T_WARP_MINES, "getRange") or 5 end, requires_target = true, getDuration = function (self, t) return getExtensionModifier(self, t, math.floor(self:combatTalentScale(t, 6, 10))) end, getChance = function(self, t) return self:combatTalentLimit(t, 30, 10, 20) end, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 150, getParadoxSpellpower(self, t)) end, target = function(self, t) return {type="hit", range=self:getTalentRange(t), nowarning=true, talent=t} 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 game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") then return end -- Tether values local power = getParadoxSpellpower(self, t) local dam = self:spellCrit(t.getDamage(self, t)) local dest_power = getParadoxSpellpower(self, t, 0.3) local range = self:getTalentRange(t) -- Store the old terrain local oe = game.level.map(target.x, target.y, engine.Map.TERRAIN) if not oe or oe:attr("temporary") then return true end -- Make our tether local tether = mod.class.Object.new{ old_feat = oe, type = oe.type, subtype = oe.subtype, name = "temporal instability", image = oe.image, add_mos = {{image="object/temporal_instability.png"}}, display = '&', color=colors.LIGHT_BLUE, temporary = t.getDuration(self, t), power = power, dest_power = dest_power, chance = t.getChance(self, t), x = x, y = y, target = target, talent = t, range = range, dam =dam, summoner = self, summoner_gain_exp = true, canAct = false, energy = {value=0}, act = function(self) self:useEnergy() self.temporary = self.temporary - 1 if not self.target.dead and (game.level and game.level:hasEntity(self.target)) then -- Warp Beam local tg = {type="beam", start_x=self.x, start_y=self.y, talent=talent, range=self.range} self.summoner:project(tg, self.target.x, self.target.y, engine.DamageType.WARP, self.dam) game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(self.target.x-self.x), math.abs(self.target.y-self.y)), "temporal_lightning", {tx=self.target.x-self.x, ty=self.target.y-self.y}) game:playSoundNear(self, "talents/lightning") -- Teleport local hit = self.summoner == self.target or (self.summoner:checkHit(self.power, self.target:combatSpellResist() + (self.target:attr("continuum_destabilization") or 0), 0, 95) and self.target:canBe("teleport")) if hit and rng.percent(self.chance * core.fov.distance(self.x, self.y, self.target.x, self.target.y)) then game.level.map:particleEmitter(self.target.x, self.target.y, 1, "temporal_teleport") -- Since we're using a precise teleport, find a free grit first local tx, ty = util.findFreeGrid(self.x, self.y, 5, true, {[engine.Map.ACTOR]=true}) if not self.target:teleportRandom(tx, ty, 1, 0) then game.logSeen(self, "The teleport fizzles!") else if self.target ~= self.summoner then self.target:setEffect(self.target.EFF_CONTINUUM_DESTABILIZATION, 100, {power=self.dest_power}) end game.level.map:particleEmitter(self.target.x, self.target.y, 1, "temporal_teleport") game:playSoundNear(self, "talents/teleport") end else game.logSeen(self.target, "%s resists the teleport!", self.target.name:capitalize()) end end -- End the effect? if self.temporary <= 0 or self.target.dead then game.level.map(self.x, self.y, engine.Map.TERRAIN, self.old_feat) game.nicer_tiles:updateAround(game.level, self.target.x, self.target.y) game.level:removeEntity(self) end end, } -- add our tether to the map game.level:addEntity(tether) game.level.map(x, y, Map.TERRAIN, tether) game.nicer_tiles:updateAround(game.level, x, y) game.level.map:updateMap(x, y) game:playSoundNear(self, "talents/warp") return true end, info = function(self, t) local duration = t.getDuration(self, t) local chance = t.getChance(self, t) local damage = t.getDamage(self, t)/2 return ([[Tethers the target to the location for %d turns. Each turn the tether will inflict %0.2f physical and %0.2f temporal (warp) damage to all targets between itself and the target. For each tile the target moves away from the tether it has a %d%% chance each turn of being teleported back. The damage will scale with your Spellpower.]]) :format(duration, damDesc(self, DamageType.PHYSICAL, damage), damDesc(self, DamageType.TEMPORAL, damage), chance) end, } newTalent{ name = "Dimensional Anchor", type = {"chronomancy/spacetime-folding", 3}, require = chrono_req3, points = 5, paradox = function (self, t) return getParadoxCost(self, t, 20) end, cooldown = 12, tactical = { DISABLE = 2 }, range = function(self, t) return self:callTalent(self.T_WARP_MINES, "getRange") or 5 end, radius = function(self, t) return math.floor(self:combatTalentScale(t, 2.5, 4.5)) end, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 230, getParadoxSpellpower(self, t)) end, getDuration = function(self, t) return getExtensionModifier(self, t, math.floor(self:combatTalentScale(t, 6, 10))) end, getDaze = function(self, t) return math.floor(self:combatTalentScale(t, 1, 2)) end, target = function(self, t) return {type="ball", range=self:getTalentRange(t), friendlyfire=false, radius=self:getTalentRadius(t), talent=t} end, requires_target = true, direct_hit = true, 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 particle if core.shader.allow("volumetric") then particle = {type="volumetric", args={radius=self:getTalentRadius(t)*2, kind="fast_sphere", img="moony_01", density=60, shininess=50, scrollingSpeed=-0.004}, only_one=true} else particle = {type="temporal_cloud"} end -- Add a lasting map effect local dam = self:spellCrit(t.getDamage(self, t)) game.level.map:addEffect(self, x, y, t.getDuration(self,t), DamageType.DIMENSIONAL_ANCHOR, {dam=dam, dur=1, daze=t.getDaze(self, t), src=self, apply=getParadoxSpellpower(self, t)}, self:getTalentRadius(t), 5, nil, particle, nil, false, false ) game:playSoundNear(self, "talents/warp") return true end, info = function(self, t) local damage = t.getDamage(self, t)/2 local radius = self:getTalentRadius(t) local duration = t.getDuration(self, t) local daze = t.getDaze(self, t) return ([[Create a radius %d anti-teleport field for %d turns. Enemies in the field will be anchored, preventing teleportation and taking %0.2f physical and %0.2f temporal (warp) damage, as well as becoming dazed for %d turns, on teleport attempts. The damage will scale with your Spellpower.]]):format(radius, duration, damDesc(self, DamageType.PHYSICAL, damage), damDesc(self, DamageType.TEMPORAL, damage), daze) end, } newTalent{ name = "Banish", type = {"chronomancy/spacetime-folding", 4}, require = chrono_req4, points = 5, paradox = function (self, t) return getParadoxCost(self, t, 12) end, cooldown = 10, tactical = { ESCAPE = 2 }, range = function(self, t) return self:callTalent(self.T_WARP_MINES, "getRange") or 5 end, radius = function(self, t) return math.floor(self:combatTalentScale(t, 2.5, 5.5)) end, getTeleport = function(self, t) return math.floor(self:combatTalentScale(t, 8, 16)) end, target = function(self, t) return {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), selffire=false, talent=t} end, requires_target = true, direct_hit = true, 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 hit = false self:project(tg, x, y, function(px, py) local target = game.level.map(px, py, Map.ACTOR) if not target or target == self then return end game.level.map:particleEmitter(target.x, target.y, 1, "temporal_teleport") if self:checkHit(getParadoxSpellpower(self, t), target:combatSpellResist() + (target:attr("continuum_destabilization") or 0)) and target:canBe("teleport") then if not target:teleportRandom(target.x, target.y, self:getTalentRadius(t) * 4, self:getTalentRadius(t) * 2) then game.logSeen(target, "The spell fizzles on %s!", target.name:capitalize()) else target:setEffect(target.EFF_CONTINUUM_DESTABILIZATION, 100, {power=getParadoxSpellpower(self, t, 0.3)}) game.level.map:particleEmitter(target.x, target.y, 1, "temporal_teleport") hit = true end else game.logSeen(target, "%s resists the banishment!", target.name:capitalize()) end end) if not hit then game:onTickEnd(function() if not self:attr("no_talents_cooldown") then self.talents_cd[self.T_BANISH] = self.talents_cd[self.T_BANISH] /2 end end) end game:playSoundNear(self, "talents/teleport") return true end, info = function(self, t) local radius = self:getTalentRadius(t) local range = t.getTeleport(self, t) return ([[Randomly teleports all targets within a radius of %d. Targets will be teleported between %d and %d tiles from their current location. If no targets are teleported the cooldown will be halved. The chance of teleportion will scale with your Spellpower.]]):format(radius, range / 2, range) end, }