-- 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 newTalent{ name = "Thread Walk", type = {"chronomancy/threaded-combat", 1}, require = chrono_req_high1, points = 5, cooldown = 10, paradox = function (self, t) return getParadoxCost(self, t, 10) end, tactical = { ATTACK = {weapon = 2}, CLOSEIN = 2, ESCAPE = 2 }, requires_target = true, is_teleport = true, range = function(self, t) if self:hasArcheryWeapon("bow") then return util.getval(archery_range, self, t) end return 1 end, is_melee = function(self, t) return not self:hasArcheryWeapon("bow") end, speed = function(self, t) return self:hasArcheryWeapon("bow") and "archery" or "weapon" end, getDamage = function(self, t) return self:combatTalentWeaponDamage(t, 1, 1.5) end, getDefense = function(self, t) return self:combatTalentStatDamage(t, "mag", 10, 25) end, getResist = function(self, t) return self:combatTalentStatDamage(t, "mag", 5, 15) end, on_pre_use = function(self, t, silent) if self:attr("disarmed") then if not silent then game.logPlayer(self, "You require a weapon to use this talent.") end return false end return true end, passives = function(self, t, p) self:talentTemporaryValue(p, "defense_on_teleport", t.getDefense(self, t)) self:talentTemporaryValue(p, "resist_all_on_teleport", t.getResist(self, t)) end, callbackOnStatChange = function(self, t, stat, v) if stat == self.STAT_MAG then self:updateTalentPassives(t) end end, archery_onreach = function(self, t, x, y) game:onTickEnd(function() game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") game:playSoundNear(self, "talents/teleport") if self:teleportRandom(x, y, 0) then game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") else game.logSeen(self, "The spell fizzles!") end end) end, action = function(self, t) local mainhand, offhand = self:hasDualWeapon() if self:hasArcheryWeapon("bow") then -- Ranged attack local targets = self:archeryAcquireTargets({type="bolt"}, {one_shot=true, no_energy = true}) if not targets then return end self:archeryShoot(targets, t, {type="bolt"}, {mult=t.getDamage(self, t)}) elseif mainhand then -- Melee attack local tg = {type="hit", range=self:getTalentRange(t), talent=t} local _, x, y = self:canProject(tg, self:getTarget(tg)) local target = game.level.map(x, y, game.level.map.ACTOR) if not target then return nil end self:attackTarget(target, nil, t.getDamage(self, t), true) -- Find a good spot to shoot from local range = 5 local weapon = self:hasArcheryWeaponQS() if weapon then range = weapon.combat.range end local poss = {} game.logPlayer(self, "range %d", range) for i = x - range, x + range do for j = y - range, y + range do if game.level.map:isBound(i, j) and core.fov.distance(x, y, i, j) <= range and -- make sure they're within arrow range core.fov.distance(i, j, self.x, self.y) >= range/2 and self:canMove(i, j) and target:hasLOS(i, j) then poss[#poss+1] = {i,j} end end end if #poss == 0 then return game.logSeen(self, "The spell fizzles!") end local pos = poss[rng.range(1, #poss)] x, y = pos[1], pos[2] game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") game:playSoundNear(self, "talents/teleport") if self:teleportRandom(x, y, 0) then game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") else game.logSeen(self, "The spell fizzles!") end -- This teleports the target straight back. Should probably copy this function someplace fun so we can use it for other stuff -- Find our teleport location --[[local dist = 10 / core.fov.distance(x, y, self.x, self.y) local destx, desty = math.floor((self.x - x) * dist + x), math.floor((self.y - y) * dist + y) local l = core.fov.line(x, y, destx, desty, false) local lx, ly, is_corner_blocked = l:step() local ox, oy while game.level.map:isBound(lx, ly) and not game.level.map:checkEntity(lx, ly, Map.TERRAIN, "block_move") and not is_corner_blocked do if not game.level.map(lx, ly, Map.ACTOR) then ox, oy = lx, ly end lx, ly, is_corner_blocked = l:step() end game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") game:playSoundNear(self, "talents/teleport") -- ox, oy now contain the last square in line not blocked by actors. if ox and oy then self:teleportRandom(ox, oy, 0) game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") end]] else game.logPlayer(self, "You cannot use Thread Walk without an appropriate weapon!") return nil end return true end, info = function(self, t) local damage = t.getDamage(self, t) * 100 local defense = t.getDefense(self, t) local resist = t.getResist(self, t) return ([[Attack with your bow or dual-weapons for %d%% damage. If you shoot an arrow you'll teleport near the target location. If you use your dual-weapons you'll teleport up to your bow's range away. Additionally you now go Out of Phase for five turns after any teleport, gaining %d defense and %d%% resist all. The Out of Phase bonuses will scale with your Magic stat.]]) :format(damage, defense, resist) end } newTalent{ name = "Blended Threads", type = {"chronomancy/threaded-combat", 2}, require = chrono_req_high2, mode = "passive", points = 5, getCount = function(self, t) return math.ceil(self:getTalentLevel(t))end, callbackOnArcheryAttack = function(self, t, target, hitted) if hitted then if self.turn_procs.blended_threads and self.turn_procs.blended_threads >= t.getCount(self, t) then return end -- Refresh talent local tids = {} for tid, _ in pairs(self.talents_cd) do local tt = self:getTalentFromId(tid) if tt.type[1]:find("^chronomancy/blade") and not tt.fixed_cooldown then tids[#tids+1] = tt end end if #tids > 0 then local tid = rng.tableRemove(tids) self:alterTalentCoolingdown(tid, - 1) self.turn_procs.blended_threads = (self.turn_procs.blended_threads or 0) + 1 end end end, callbackOnMeleeAttack = function(self, t, target, hitted) if hitted then if self.turn_procs.blended_threads and self.turn_procs.blended_threads >= t.getCount(self, t) then return end -- Refresh talent local tids = {} for tid, _ in pairs(self.talents_cd) do local tt = self:getTalentFromId(tid) if tt.type[1]:find("^chronomancy/bow") and not tt.fixed_cooldown then tids[#tids+1] = tt end end if #tids > 0 then local tid = rng.tableRemove(tids) self:alterTalentCoolingdown(tid, - 1) self.turn_procs.blended_threads = (self.turn_procs.blended_threads or 0) + 1 end end end, info = function(self, t) local count = t.getCount(self, t) return ([[Each time you hit with an arrow you reduce the cooldown of one Blade Threading talent on cooldown by one turn. Each time you hit with a melee weapon you reduce the cooldown of one Bow Threading talent on cooldown by one turn. This effect can only occur %d times per turn.]]) :format(count) end } newTalent{ name = "Thread the Needle", type = {"chronomancy/threaded-combat", 3}, require = chrono_req_high3, points = 5, cooldown = 8, paradox = function (self, t) return getParadoxCost(self, t, 18) end, tactical = { ATTACKAREA = { weapon = 3 } }, requires_target = true, range = function(self, t) if self:hasArcheryWeapon("bow") then return util.getval(archery_range, self, t) end return 0 end, is_melee = function(self, t) return not self:hasArcheryWeapon("bow") end, speed = function(self, t) return self:hasArcheryWeapon("bow") and "archery" or "weapon" end, getDamage = function(self, t) return self:combatTalentWeaponDamage(t, 1.1, 2.2) end, on_pre_use = function(self, t, silent) if self:attr("disarmed") then if not silent then game.logPlayer(self, "You require a weapon to use this talent.") end return false end return true end, target = function(self, t) local tg = {type="beam", range=self:getTalentRange(t)} if not self:hasArcheryWeapon("bow") then tg = {type="ball", radius=1, range=self:getTalentRange(t)} end return tg end, action = function(self, t) local tg = self:getTalentTarget(t) local damage = t.getDamage(self, t) local mainhand, offhand = self:hasDualWeapon() if self:hasArcheryWeapon("bow") then -- Ranged attack local targets = self:archeryAcquireTargets(tg, {one_shot=true, no_energy = true}) if not targets then return end self:archeryShoot(targets, t, tg, {mult=damage}) elseif mainhand then -- Melee attack self:project(tg, self.x, self.y, function(px, py, tg, self) local target = game.level.map(px, py, Map.ACTOR) if target and target ~= self then self:attackTarget(target, nil, damage, true) end end) self:addParticles(Particles.new("meleestorm2", 1, {})) else game.logPlayer(self, "You cannot use Thread the Needle without an appropriate weapon!") return nil end return true end, info = function(self, t) local damage = t.getDamage(self, t) * 100 return ([[Attack with your bow or dual-weapons for %d%% damage. If you use your bow you'll shoot all targets in a beam. If you use your dual-weapons you'll attack all targets within a radius of one around you.]]) :format(damage) end } newTalent{ name = "Warden's Call", short_name = "WARDEN_S_CALL", type = {"chronomancy/threaded-combat", 4}, require = chrono_req_high4, mode = "passive", points = 5, remove_on_clone = true, getDamagePenalty = function(self, t) return 100 - self:combatTalentLimit(t, 80, 10, 60) end, findTarget = function(self, t) local tgts = {} local grids = core.fov.circle_grids(self.x, self.y, 10, true) for x, yy in pairs(grids) do for y, _ in pairs(grids[x]) do local target_type = Map.ACTOR local a = game.level.map(x, y, Map.ACTOR) if a and not a.dead and self:reactionToward(a) < 0 and self:hasLOS(a.x, a.y) then tgts[#tgts+1] = a end end end return tgts end, cleanupClone = function(self, t, clone) if not self or not clone then return false end if not clone.dead then clone:die() end for _, ent in pairs(game.level.entities) do -- Replace clone references in timed effects so they don't prevent GC if ent.tmp then for _, eff in pairs(ent.tmp) do if eff.src and eff.src == clone then eff.src = self end end end end return true end, callbackOnArcheryAttack = function(self, t, target, hitted) if hitted then if self.turn_procs.wardens_call then return else self.turn_procs.wardens_call = true end -- Make our clone local m = makeParadoxClone(self, self, 0) m.generic_damage_penalty = (m.generic_damage_penalty or 0) + t.getDamagePenalty(self, t) doWardenWeaponSwap(m, t, "blade") m.on_added_to_level = function(self) if not self.blended_target.dead then self:forceUseTalent(self.T_ATTACK, {ignore_cd=true, ignore_energy=true, force_target=self.blended_target, ignore_ressources=true, silent=true}) end self:die() game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") end -- Check Focus first local wf = checkWardenFocus(self) if wf and not wf.dead then local tx, ty = util.findFreeGrid(wf.x, wf.y, 1, true, {[Map.ACTOR]=true}) if tx and ty then m.blended_target = wf game.zone:addEntity(game.level, m, "actor", tx, ty) end end if not m.blended_target then local tgts= t.findTarget(self, t) local attempts = 5 while #tgts > 0 and attempts > 0 do local a, id = rng.tableRemove(tgts) -- look for space local tx, ty = util.findFreeGrid(a.x, a.y, 1, true, {[Map.ACTOR]=true}) if tx and ty then m.blended_target = a game.zone:addEntity(game.level, m, "actor", tx, ty) break else attempts = attempts - 1 end end end t.cleanupClone(self, t, m) end end, callbackOnMeleeAttack = function(self, t, target, hitted) if hitted then if self.turn_procs.wardens_call then return else self.turn_procs.wardens_call = true end -- Make our clone local m = makeParadoxClone(self, self, 0) m.generic_damage_penalty = (m.generic_damage_penalty or 0) + t.getDamagePenalty(self, t) m:attr("archery_pass_friendly", 1) doWardenWeaponSwap(m, t, "bow") m.on_added_to_level = function(self) if not self.blended_target.dead then local targets = self:archeryAcquireTargets(nil, {one_shot=true, x=self.blended_target.x, y=self.blended_target.y, no_energy = true}) if targets then self:forceUseTalent(self.T_SHOOT, {ignore_cd=true, ignore_energy=true, force_target=self.blended_target, ignore_ressources=true, silent=true}) end end self:die() game.level.map:particleEmitter(self.x, self.y, 1, "temporal_teleport") end -- Find a good location for our shot local function find_space(self, target, clone) local poss = {} local range = util.getval(archery_range, clone, t) local x, y = target.x, target.y for i = x - range, x + range do for j = y - range, y + range do if game.level.map:isBound(i, j) and core.fov.distance(x, y, i, j) <= range and -- make sure they're within arrow range core.fov.distance(i, j, self.x, self.y) <= range/2 and -- try to place them close to the caster so enemies dodge less self:canMove(i, j) and target:hasLOS(i, j) then poss[#poss+1] = {i,j} end end end if #poss == 0 then return end local pos = poss[rng.range(1, #poss)] x, y = pos[1], pos[2] return x, y end -- Check Focus first local wf = checkWardenFocus(self) if wf and not wf.dead then local tx, ty = find_space(self, target, m) if tx and ty then m.blended_target = wf game.zone:addEntity(game.level, m, "actor", tx, ty) end else local tgts = t.findTarget(self, t) if #tgts > 0 then local a, id = rng.tableRemove(tgts) local tx, ty = find_space(self, target, m) if tx and ty then m.blended_target = a game.zone:addEntity(game.level, m, "actor", tx, ty) end end end t.cleanupClone(self, t, m) end end, info = function(self, t) local damage_penalty = t.getDamagePenalty(self, t) return ([[When you hit with a melee or arrow attack a warden may appear from another timeline, depending on available space, and shoot or attack a random enemy. The wardens are out of phase with this reality and deal %d%% less damage but the bow warden's arrows will pass through friendly targets. This effect can only occur once per turn and the wardens return to their own timeline after attacking.]]) :format(damage_penalty) end }