diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua index b4ca085b488d5bf467161f95bc9b79fccfa1dbd2..e4fb8ddfbb1176895433a2365c4d14cf9e953b55 100644 --- a/game/modules/tome/class/Actor.lua +++ b/game/modules/tome/class/Actor.lua @@ -1406,6 +1406,7 @@ function _M:move(x, y, force) else local speed = self:combatMovementSpeed(x, y) local use_energy = true + if self:attr("free_movement") then use_energy = false end if self:attr("walk_sun_path") then for i, e in ipairs(game.level.map.effects) do if e.damtype == DamageType.SUN_PATH and e.grids[x] and e.grids[x][y] then use_energy = false break end end end @@ -6868,13 +6869,13 @@ end --- Checks if the talent can be learned function _M:canLearnTalent(t, ...) - local ret = engine.interface.ActorTalents.canLearnTalent(self, t, ...) + local status, reason = engine.interface.ActorTalents.canLearnTalent(self, t, ...) if (t.is_class_evolution or t.is_race_evolution) and self:attr("has_evolution") then if not self.is_in_uber_dialog or not self.is_in_uber_dialog.levelup_end_prodigies or not self.is_in_uber_dialog.levelup_end_prodigies[t.id] then return nil, _t"can only learn one evolution" end end - return ret + return status, reason end --- Formats the requirements as a (multiline) string diff --git a/game/modules/tome/data/damage_types.lua b/game/modules/tome/data/damage_types.lua index fb78a357a2c3b0f7de25d2c9bc8da29815d040aa..2a0b14f22a949a71f817f57cefa67c005598747f 100644 --- a/game/modules/tome/data/damage_types.lua +++ b/game/modules/tome/data/damage_types.lua @@ -4384,3 +4384,25 @@ newDamageType{ end end, } + +-- Unresistible damage, always uses highest resistance penetration +-- NEVER add items that resist that! Use sparingly! +newDamageType{ + name = _t"thaumic energy", type = "THAUM", text_color = "#C259D0#", + death_message = {_t"utterly vaporized", _t"annihilated", _t"disintegrated"}, + projector = function(src, x, y, type, dam, state) + state = initState(state) + useImplicitCrit(src, state) + if src.combatGetResistPen then + if not src.auto_highest_resists_pen then src.auto_highest_resists_pen = {} end + if not src.auto_highest_resists_pen[DamageType.THAUM] then src.auto_highest_resists_pen[DamageType.THAUM] = 1 end + end + if src.combatGetDamageIncrease then + if not src.auto_highest_inc_damage then src.auto_highest_inc_damage = {} end + if not src.auto_highest_inc_damage[DamageType.THAUM] then src.auto_highest_inc_damage[DamageType.THAUM] = 1 end + end + local target = game.level.map(x, y, Map.ACTOR) + if target and target:hasEffect(target.EFF_WET) then dam = dam * 1.3 end + return DamageType.defaultProjector(src, x, y, type, dam, state) + end, +} diff --git a/game/modules/tome/data/gfx/talents/elemental_array_burst.png b/game/modules/tome/data/gfx/talents/elemental_array_burst.png new file mode 100644 index 0000000000000000000000000000000000000000..9c3e5e254511db2987136fe089d729b8fc892b76 Binary files /dev/null and b/game/modules/tome/data/gfx/talents/elemental_array_burst.png differ diff --git a/game/modules/tome/data/gfx/talents/multicaster.png b/game/modules/tome/data/gfx/talents/multicaster.png new file mode 100644 index 0000000000000000000000000000000000000000..a551e45627a79f3b8373b4ae50cfd0835952f581 Binary files /dev/null and b/game/modules/tome/data/gfx/talents/multicaster.png differ diff --git a/game/modules/tome/data/gfx/talents/orb_of_thaumaturgy.png b/game/modules/tome/data/gfx/talents/orb_of_thaumaturgy.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8206b040b0e478cd19e8ba282d9ee8e8c3aa27 Binary files /dev/null and b/game/modules/tome/data/gfx/talents/orb_of_thaumaturgy.png differ diff --git a/game/modules/tome/data/gfx/talents/slipstream.png b/game/modules/tome/data/gfx/talents/slipstream.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb1643dd20064ff43d4b81cf76ec212925e92d0 Binary files /dev/null and b/game/modules/tome/data/gfx/talents/slipstream.png differ diff --git a/game/modules/tome/data/talents/spells/air.lua b/game/modules/tome/data/talents/spells/air.lua index 1be809561dd6e9f363a744100758d0c06d96027c..cd285f23142fd3bfc018afa98a37b48feca98c8e 100644 --- a/game/modules/tome/data/talents/spells/air.lua +++ b/game/modules/tome/data/talents/spells/air.lua @@ -30,10 +30,11 @@ newTalent{ reflectable = true, requires_target = true, target = function(self, t) - if self:attr("archmage_widebeam") then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end + if thaumaturgyCheck(self) then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end return {type="beam", range=self:getTalentRange(t), talent=t} end, allow_for_arcane_combat = true, + is_beam_spell = true, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 20, 350) end, action = function(self, t) local tg = self:getTalentTarget(t) @@ -43,7 +44,7 @@ newTalent{ self:project(tg, x, y, DamageType.LIGHTNING_DAZE, {dam=rng.avg(dam / 3, dam, 3), daze=self:attr("lightning_daze_tempest") or 0}) local _ _, x, y = self:canProject(tg, x, y) - if self:attr("archmage_widebeam") then + if thaumaturgyCheck(self) then game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(x-self.x), math.abs(y-self.y)), "lightning_beam_wide", {tx=x-self.x, ty=y-self.y}, core.shader.active() and {type="lightning"} or nil) else game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(x-self.x), math.abs(y-self.y)), "lightning_beam", {tx=x-self.x, ty=y-self.y}, core.shader.active() and {type="lightning"} or nil) diff --git a/game/modules/tome/data/talents/spells/arcane.lua b/game/modules/tome/data/talents/spells/arcane.lua index d325fb0d391fc061a2270d461bd5d2f629b87efc..3fce7948e0ff3dc272a2f5a5b508548941def760 100644 --- a/game/modules/tome/data/talents/spells/arcane.lua +++ b/game/modules/tome/data/talents/spells/arcane.lua @@ -32,9 +32,10 @@ newTalent{ proj_speed = 20, direct_hit = function(self, t) if self:getTalentLevel(t) >= 3 then return true else return false end end, reflectable = true, + is_beam_spell = true, requires_target = true, target = function(self, t) - if self:attr("archmage_widebeam") then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end + 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"}} if self:getTalentLevel(t) >= 3 then tg.type = "beam" end return tg @@ -47,7 +48,7 @@ newTalent{ if tg.type == "beam" or tg.type == "widebeam" then self:project(tg, x, y, DamageType.ARCANE, self:spellCrit(t.getDamage(self, t)), nil) local _ _, x, y = self:canProject(tg, x, y) - if self:attr("archmage_widebeam") then + if thaumaturgyCheck(self) then 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}) diff --git a/game/modules/tome/data/talents/spells/earth.lua b/game/modules/tome/data/talents/spells/earth.lua index 4919c473515788645c3e6803d88acb2693cb109f..cb788855bba6e5590a86d242d92fa259308a4d46 100644 --- a/game/modules/tome/data/talents/spells/earth.lua +++ b/game/modules/tome/data/talents/spells/earth.lua @@ -30,10 +30,11 @@ newTalent{ is_body_of_stone_affected = true, range = 10, tactical = { ATTACK = {PHYSICAL = 2} }, + is_beam_spell = true, direct_hit = true, requires_target = true, target = function(self, t) - if self:attr("archmage_widebeam") then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end + if thaumaturgyCheck(self) then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end local tg = {type="beam", range=self:getTalentRange(t), talent=t} return tg end, @@ -55,7 +56,7 @@ newTalent{ tg.range = self:getTalentRange(t) self:project(tg, x, y, DamageType.PHYSICAL, self:spellCrit(t.getDamage(self, t)), nil) local _ _, x, y = self:canProject(tg, x, y) - if self:attr("archmage_widebeam") then + if thaumaturgyCheck(self) then game.level.map:particleEmitter(self.x, self.y, math.max(math.abs(x-self.x), math.abs(y-self.y)), "earth_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)), "earth_beam", {tx=x-self.x, ty=y-self.y}) diff --git a/game/modules/tome/data/talents/spells/fire.lua b/game/modules/tome/data/talents/spells/fire.lua index 14e9982e4d2b2f9a0bd4f08b018a6e9e79c37dde..785c5393aebd55d18e88ed8589da81e50223b42f 100644 --- a/game/modules/tome/data/talents/spells/fire.lua +++ b/game/modules/tome/data/talents/spells/fire.lua @@ -28,10 +28,11 @@ newTalent{ tactical = { ATTACK = { FIRE = 2 } }, range = 10, reflectable = true, + is_beam_spell = true, proj_speed = 20, requires_target = true, target = function(self, t) - if self:attr("archmage_widebeam") then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end + 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_fire", trail="firetrail"}} if self:getTalentLevel(t) >= 5 then tg.type = "beam" end return tg @@ -45,7 +46,7 @@ newTalent{ if not x or not y then return nil end local grids = nil - if self:attr("archmage_widebeam") then + if thaumaturgyCheck(self) then grids = self:project(tg, x, y, DamageType.FIREBURN, self:spellCrit(t.getDamage(self, t))) local _ _, x, y = self:canProject(tg, x, y) game.level.map:particleEmitter(self.x, self.y, tg.radius, "flamebeam_wide", {tx=x-self.x, ty=y-self.y}) diff --git a/game/modules/tome/data/talents/spells/spells.lua b/game/modules/tome/data/talents/spells/spells.lua index e3bbf5d750159f5522e5d903cfeca874ad9d6c3a..48a1580f45e8002dfb6d0e1f0ee24dcf0f54a205 100644 --- a/game/modules/tome/data/talents/spells/spells.lua +++ b/game/modules/tome/data/talents/spells/spells.lua @@ -119,6 +119,15 @@ spells_req_high5 = { level = function(level) return 26 + (level-1) end, } +function thaumaturgyCheck(self) + if not self:attr("archmage_widebeam") then return false end + local inven = self:getInven("BODY") + if not inven then return true end + if not inven[1] then return true end + if inven[1].type ~= "armor" or inven[1].subtype ~= "cloth" then return false end + return true +end + ------------------------------------------- -- Necromancer minions function necroArmyStats(self) diff --git a/game/modules/tome/data/talents/spells/thaumaturgy.lua b/game/modules/tome/data/talents/spells/thaumaturgy.lua index 167f245dedf35ae672fd74a5c2a8e7a374ab92c4..9f8b51737ed01933838e8d583b674c3020bbb387 100644 --- a/game/modules/tome/data/talents/spells/thaumaturgy.lua +++ b/game/modules/tome/data/talents/spells/thaumaturgy.lua @@ -22,16 +22,27 @@ newTalent{ type = {"spell/thaumaturgy",1}, require = spells_req_high1, points = 5, - mana = 40, + mana = 35, cooldown = 20, + range = 10, use_only_arcane = 5, tactical = { BUFF=2 }, + on_pre_use = thaumaturgyCheck, + requires_target = true, + no_energy = true, + target = function(self, t) return {type="hit", range=self:getTalentRange(t)} end, + getDur = function(self, t) return math.floor(self:combatTalentScale(t, 2, 6)) end, action = function(self, t) + local tg = self:getTalentTarget(t) + local x, y = self:getTargetLimitedWallStop(tg) + if not x or not y then return nil end + self:setEffect(self.EFF_ORB_OF_THAUMATURGY, t:_getDur(self), {x=x, y=y}) return true end, info = function(self, t) - return ([[ - ]]):tformat() + return ([[You create an orb attuned to thaumaturgy for %d turns. + While it lasts, any beam spell you cast will be duplicated and also cast for free at the orb. + ]]):tformat(t:_getDur(self)) end, } @@ -40,164 +51,124 @@ newTalent{ type = {"spell/thaumaturgy",2}, require = spells_req_high2, points = 5, - mana = 30, - cooldown = 6, + mode = "sustained", + sustain_mana = 70, + cooldown = 30, use_only_arcane = 5, - tactical = { ATTACKAREA = { TEMPORAL = 2, ARCANE = 2 }, }, - range = 10, - target = function(self, t) return {type="widebeam", force_max_range=true, radius=1, range=self:getTalentRange(t), selffire=false, talent=t} end, - getSlow = function(self, t) return math.min(self:getTalentLevel(t) * 0.05, 0.5) end, - getProj = function(self, t) return math.min(90, 5 + self:combatTalentSpellDamage(t, 5, 500) / 10) end, - getDamage = function(self, t) return self:combatTalentSpellDamage(t, 28, 370) end, - on_pre_use = function(self, t, silent) - if not self:knowTalent(self.T_TINKER_ARCANE_DYNAMO) then if not silent then game.logPlayer(self, "You need an arcane dynamo to cast this spell.") end return false end - if not self:isTalentActive(self.T_METATEMPORAL_SPINNER) then if not silent then game.logPlayer(self, "You need to activate Metatemporal Spinner to cast this spell.") end return false end - return true - end, - callbackOnCloned = function(self, t) - self._reality_breach_remain = nil - self._reality_breach_desc = nil - end, - callbackOnChangeLevel = function(self, t, what) - self._reality_breach_remain = nil - self._reality_breach_desc = nil - end, - callbackOnAct = function(self, t) - if self._reality_breach_remain then - self._reality_breach_remain = self._reality_breach_remain - 1 - if self._reality_breach_remain <= 0 then - self._reality_breach_remain = nil - self._reality_breach_desc = nil - end + tactical = { BUFF = 1 }, + on_pre_use = thaumaturgyCheck, + getChance = function(self, t) return self:combatTalentLimit(t, 100, 10, 30) end, + spells_list = { + ["spell/fire"] = { "T_FLAMESHOCK", "T_FIREFLASH", "T_INFERNO", "T_BLASTWAVE"}, + ["spell/arcane"] = { "T_ARCANE_VORTEX", "T_AETHER_BEAM", "T_AETHER_BREACH"}, + ["spell/lightning"] = { "T_CHAIN_LIGHTNING", "T_NOVA", "T_SHOCK" }, + ["spell/water"] = { "T_GLACIAL_VAPOUR", "T_TIDAL_WAVE", "T_FREEZE", "T_FROZEN_GROUND" }, + ["spell/earth"] = { "T_MUDSLIDE", "T_EARTHEN_MISSILES", "T_EARTHQUAKE" }, + }, + callbackOnDealDamage = function(self, t, val, target, dead, death_note) + if not death_note then return end + if death_note.source_talent_mode ~= "active" and not self._orb_of_thaumaturgy_recurs then return end + if not death_note.source_talent or not death_note.source_talent.is_beam_spell then return end + + local tids = {} + for kind, list in pairs(t.spells_list) do + for _, tid in ipairs(list) do if self:knowTalent(tid) then + -- print(("[%s] %s : %d"):format(kind, tid, self:getTalentCooldown(self:getTalentFromId(tid), true))) + tids[#tids+1] = {tid=tid, cd=self:getTalentCooldown(self:getTalentFromId(tid), true)} + end end end + local choice = rng.rarityTable(tids, "cd") + if not choice then return end + table.print(self._orb_of_thaumaturgy_recurs) + self:forceUseTalent(choice.tid, {ignore_cooldown=true, ignore_energy=true, force_target=self._orb_of_thaumaturgy_recurs or target}) + end, + activate = function(self, t) + local ret = {} + return ret 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) - if core.fov.distance(self.x, self.y, x, y) < 1 then return nil end - - local tgts = {} - local dam = self:spellCrit(t.getDamage(self, t)) - local slow = t.getSlow(self, t) - local proj = t.getProj(self, t) - self:project(tg, x, y, function(px, py) - DamageType:get(DamageType.OCCULT).projector(self, px, py, DamageType.OCCULT, dam) - local target = game.level.map(px, py, Map.ACTOR) - if target then tgts[target] = true end - end) - - local sorted_tgts = {} - for target, _ in pairs(tgts) do - sorted_tgts[#sorted_tgts+1] = {target=target, dist=core.fov.distance(self.x, self.y, target.x, target.y)} - end - table.sort(sorted_tgts, "dist") - - -- Compute beam direction to knockback all targets in that direction even if they are on the sides of the beam - local angle = math.atan2(y - self.y, x - self.x) - - for _, tgt in ripairs(sorted_tgts) do - local target = tgt.target - if target:canBe("slow") then - target:setEffect(target.EFF_CONGEAL_TIME, 4, {slow=slow, proj=proj, apply_power=self:combatSpellpower()}) - end - if self:getTalentLevel(t) >= 5 and target:canBe("knockback") then - target:pull(target.x + math.cos(angle) * 10, target.y + math.sin(angle) * 10, 3) - end - end - - if self:getTalentLevel(t) >= 3 then - self:projectApply(tg, x, y, Map.PROJECTILE, function(proj, px, py) - proj:terminate(px, py) - game.level:removeEntity(proj, true) - proj.dead = true - self:logCombat(proj, "#Source# annihilates '#Target#'!") - end) - end - - game.level.map:particleEmitter(self.x, self.y, 10, "time_breach", {tx=x-self.x, ty=y-self.y}) - game.level.map:particleEmitter(self.x, self.y, 10, "time_breach", {tx=x-self.x, ty=y-self.y}) - game.level.map:particleEmitter(self.x, self.y, 10, "time_breach3", {tx=x-self.x, ty=y-self.y}) - game.level.map:particleEmitter(self.x, self.y, 10, "time_breach2", {tx=x-self.x, ty=y-self.y}) - if core.shader.allow("distort") then - game.level.map:particleEmitter(self.x, self.y, 10, "time_breach_distort", {tx=x-self.x, ty=y-self.y}) - end - - game:shakeScreen(10, 3) - game:playSoundNear(self, "talents/reality_breach") - - local effect_desc = { - from = {x=self.x, y=self.y}, - to = {x=x, y=y}, - range = self:getTalentRange(t), - } - - if self:hasEffect(self.EFF_METAPHASIC_ECHO) then - local eff = self:hasEffect(self.EFF_METAPHASIC_ECHO) - eff.list[#eff.list+1] = effect_desc - end - - self._reality_breach_desc = effect_desc - self._reality_breach_remain = 2 - + deactivate = function(self, t) return true end, info = function(self, t) - local damage = t.getDamage(self, t) - local slow = t.getSlow(self, t) - local proj = t.getProj(self, t) - return ([[Spin your saw at incredible speeds for an instant, fully breaking reality in a 3-wide beam in front of you. - Any creatures caught by the beam take %0.2f occult damage and are untethered from reality, reducing their global speed by %d%% and the speed of any projectiles they fire by %d%% for 4 turns. - At level 3 any projectiles caught in the beam are instantly annihilated. - At level 5 the beam is so strong that all creatures caught inside are knocked back 3 tiles. - The breach is so deep that the beam will always have the maximum possible length it can. - The damage will increase with your Spellpower.]]):tformat(damDesc(self, DamageType.OCCULT, damage), 100 * slow, proj) + return ([[Casting beam spells has become so instinctive for you that you can now easily weave in other spells at the same time. + Anytime you cast a beam spell there is a %d%% chance to automatically cast an offensive spell that you know. This can only happen once per turn. + Beam spells duplicated by the Orb of Thaumaturgy can also trigger this effect. + The additional cast will cost mana but no turn and will not active its cooldown.]]): + tformat(t:_getChance(self)) end, } newTalent{ - name = "Splipstream", + name = "Slipstream", type = {"spell/thaumaturgy",3}, require = spells_req_high3, points = 5, + mode = "sustained", mana = 40, - cooldown = 12, + cooldown = 20, use_only_arcane = 5, - tactical = { ATTACKAREA = { ARCANE = 1, TEMPORAL = 1 }, }, - radius = 10, - target = function(self, t) return {type="ball", radius=self:getTalentRadius(t), talent=t} end, - getDur = function(self, t) return self:combatTalentLimit(t, 25, 6, 15) end, - getDamage = function(self, t) return self:combatTalentSpellDamage(t, 10, 220) / 10 end, - on_pre_use = function(self, t, silent) - if not self:knowTalent(self.T_TINKER_ARCANE_DYNAMO) then if not silent then game.logPlayer(self, "You need an arcane dynamo to cast this spell.") end return false end - if not self:isTalentActive(self.T_METATEMPORAL_SPINNER) then if not silent then game.logPlayer(self, "You need to activate Metatemporal Spinner to cast this spell.") end return false end - return true + tactical = { BUFF=1 }, + getNb = function(self, t) return math.floor(self:combatTalentScale(t, 3, 9)) end, + iconOverlay = function(self, t, p) + local val = p.charges + if val <= 0 then return "" end + return tostring(math.ceil(val)), "buff_font_small" end, - action = function(self, t) - local tg = self:getTalentTarget(t) - local tgts = self:projectCollect(tg, self.x, self.y, Map.ACTOR, function(target) - return self:reactionToward(target) < 0 and target:hasEffect(target.EFF_CONGEAL_TIME) - end) - if not next(tgts) then return nil end + resetStream = function(self, t, ret) + ret.been_used = nil + ret.charges = t:_getNb(self) + end, + useStream = function(self, t) + local p = self:isTalentActive(t.id) + if not p then return end + p.charges = p.charges - 1 + p.been_used = true + if p.charges <= 0 then self:forceUseTalent(t.id, {ignore_energy=true}) end + end, + callbackOnActBase = function(self, t) + local p = self:isTalentActive(t.id) + if not p then return end - local dam = self:spellCrit(t.getDamage(self, t)) - for target, _ in pairs(tgts) do - target:setEffect(target.EFF_ETHEREAL_STEAM, t.getDur(self, t), {src=self, dam=dam}) + if p.out_combat_turn and p.out_combat_turn < game.turn - 100 and p.been_used then + local mana = util.getval(t.mana, self, t) or 0 + mana = self:alterTalentCost(t, "mana", mana) + + if self:getMana() < mana then + game.logPlayer(self, "#PURPLE#Your Slipstream does not have enough resources!") + self:forceUseTalent(t.id, {ignore_energy=true}) + else + game.logPlayer(self, "#PURPLE#Your Slipstream regenerates to full!") + t:_resetStream(self, p) + self:incMana(-mana) + end end - - game:playSoundNear(self, "talents/arcane") + end, + callbackOnCombat = function(self, t, state) + local p = self:isTalentActive(t.id) + if not p then return end + if state then p.out_combat_turn = nil + else p.out_combat_turn = game.turn + end + end, + callbackOnDealDamage = function(self, t, val, target, dead, death_note) + if not death_note then return end + if death_note.source_talent_mode ~= "active" then return end + if not death_note.source_talent or not death_note.source_talent.is_beam_spell then return end + self:setEffect(self.EFF_SLIPSTREAM, 1, {}) + end, + on_pre_use = thaumaturgyCheck, + activate = function(self, t) + local ret = { charges = t:_getNb(self) } + return ret + end, + deactivate = function(self, t) return true end, info = function(self, t) - local damage = t.getDamage(self, t) - local radius = self:getTalentRadius(t) - return ([[You reach out through the aether to all creatures in sight that were slowed by Reality Breach or Congeal Time. - For each target you create a link of arcane infused steam to it that lasts %d turns. - Any time the target uses a talent one of your cooling down spells is reduced by 1 (prioritizing Technomancy spells). - Each turn the link is up the target and any creature inside the link takes %0.2f occult damage. - As long as at least one link is up, the cooldown of your Metaphasic Spin spell is set to 6 turns instead of 30. - The damage will increase with your Spellpower.]]):tformat(t.getDur(self, t), damDesc(self, DamageType.OCCULT, damage)) + return ([[By weaving arcane triggers around you feet you can use the residual energies of your beam spells for free movement. + Each time you cast a beam spell you can move right afterwards without spending a turn. + This spell has %d charges. Once all charges are spent it unsustains. + If you exit combat with some charges left it will after 10 turn regenerates its charges, if you have enough mana.]]):tformat(t:_getNb(self)) end, } @@ -206,70 +177,74 @@ newTalent{ type = {"spell/thaumaturgy",4}, require = spells_req_high4, points = 5, - mana = 40, - cooldown = function(self, t) return self:combatTalentLimit(t, 6, 30, 20) end, + mana = 25, use_only_arcane = 5, - tactical = { ATTACKAREA = { ARCANE = 2, TEMPORAL = 2 }, DISABLE = function(self, t, aitarget) - if self:getTalentLevel(t) < 5 then return 0 end - local nb = 0 - for eff_id, p in pairs(aitarget.tmp) do - local e = self.tempeffect_def[eff_id] - if e.type ~= "other" and e.status == "beneficial" then nb = nb + 1 end - end - return nb^0.5 - end - }, + cooldown = 16, + tactical = { ATTACKAREA = { THAUM = 3 } }, range = 10, - target = function(self, t) return {type="ball", radius=self:getTalentRange(t), talent=t} end, - getReduce = function(self, t) return self:combatTalentScale(t, 1, 5) end, - getDur = function(self, t) return self:combatTalentScale(t, 3, 6) end, - getDamage = function(self, t) return self:combatTalentWeaponDamage(t, 0.4, 1.15) end, - on_pre_use = function(self, t, silent) - if not self:knowTalent(self.T_TINKER_ARCANE_DYNAMO) then if not silent then game.logPlayer(self, "You need an arcane dynamo to cast this spell.") end return false end - if not self:isTalentActive(self.T_METATEMPORAL_SPINNER) then if not silent then game.logPlayer(self, "You need to activate Metatemporal Spinner to cast this spell.") end return false end - if not self._reality_breach_remain then if not silent then game.logPlayer(self, "You can only cast this spell on the turn after Reality Breach.") end return false end - return true - end, - doEcho = function(self, t, list) - local p = self:isTalentActive(self.T_METATEMPORAL_SPINNER) - if not p then return end - local weapon = p.o + is_beam_spell = true, + requires_target = true, + target = function(self, t) return {type="widebeam", force_max_range=true, radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} end, + getDamage = function(self, t) return self:combatTalentSpellDamage(t, 28, 370) end, + callbackOnDealDamage = function(self, t, val, target, dead, death_note) + if not death_note then return end + if death_note.source_talent_mode ~= "active" then return end + if not death_note.source_talent or not death_note.source_talent.is_beam_spell or death_note.source_talent.id == t.id then return end + if self.turn_procs.elemental_array_burst then return end + self.turn_procs.elemental_array_burst = true + self:alterTalentCoolingdown(t.id, -1) + end, + on_pre_use = thaumaturgyCheck, + 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 dam_tgts = {} - for _, data in ipairs(list) do - local x, y = data.to.x, data.to.y - self:projectCollect({type="widebeam", radius=1, force_max_range=true, range=data.range, selffire=false, x=data.from.x, y=data.from.y}, x, y, Map.ACTOR, nil, dam_tgts) + local dam = self:spellCrit(t.getDamage(self, t)) + local grids = self:project(tg, x, y, DamageType.THAUM, dam) - game.level.map:particleEmitter(data.from.x, data.from.y, data.range, "time_breach", {a=0.5, tx=x-data.from.x, ty=y-data.from.y}) - game.level.map:particleEmitter(data.from.x, data.from.y, data.range, "flying_sawblade", {size_factor=2, image="particles_images/arcane_sawblade_smaller", tx=x-data.from.x, ty=y-data.from.y}) - if core.shader.allow("distort") then - game.level.map:particleEmitter(data.from.x, data.from.y, data.range, "time_breach_distort", {tx=x-data.from.x, ty=y-data.from.y}) + if self:attr("burning_wake") and grids then + for px, ys in pairs(grids) do + for py, _ in pairs(ys) do + game.level.map:addEffect(self, px, py, 4, engine.DamageType.INFERNO, self:attr("burning_wake"), 0, 5, nil, {type="inferno"}, nil, self:spellFriendlyFire()) + end end end - for target, _ in pairs(dam_tgts) do - self.turn_procs.auto_melee_hit = true - self:attackTargetWith(target, weapon.combat, DamageType.OCCULT, t.getDamage(self, t)) - self.turn_procs.auto_melee_hit = nil - target:removeEffectsFilter(function(eff) return eff.status == "beneficial" and eff.type ~= "other" end, 1, false, false, function(_, eff_id) - local p = target.tmp[eff_id] - p.dur = p.dur - t.getReduce(self, t) - if p.dur <= 0 then return true end - end) + if self:isTalentActive(self.T_HURRICANE) then + self:projectApply(tg, x, y, Map.ACTOR, function(target) if rng.percent(25) then self:callTalent(self.T_HURRICANE, "do_hurricane", target) end end) end - game:shakeScreen(4, 1) - end, - action = function(self, t) - self:setEffect(self.EFF_METAPHASIC_ECHO, t.getDur(self, t), {list={self._reality_breach_desc}}) - game:playSoundNear(self, "talents/arcane") + if self:isTalentActive(self.T_CRYSTALLINE_FOCUS) then + self:projectApply(tg, x, y, Map.ACTOR, function(target) if rng.percent(25) then + if target:canBe("stun") then target:setEffect(target.EFF_STUNNED, 3, {apply_power=self:combatSpellpower()}) end + end end) + end + if self:isTalentActive(self.T_UTTERCOLD) then + self:projectApply(tg, x, y, Map.ACTOR, function(target) if rng.percent(25) then + if target:canBe("stun") then target:setEffect(target.EFF_FROZEN, 3, {apply_power=self:combatSpellpower(), hp=dam}) end + end end) + end + + local _ _, x, y = self:canProject(tg, x, y) + game.level.map:particleEmitter(self.x, self.y, tg.radius, "flamebeam_wide", {tx=x-self.x, ty=y-self.y}) + game.level.map:particleEmitter(self.x, self.y, tg.radius, "mana_beam_wide", {tx=x-self.x, ty=y-self.y}) + game.level.map:particleEmitter(self.x, self.y, tg.radius, "ice_beam_wide", {tx=x-self.x, ty=y-self.y}) + game.level.map:particleEmitter(self.x, self.y, tg.radius, "earth_beam_wide", {tx=x-self.x, ty=y-self.y}) + game.level.map:particleEmitter(self.x, self.y, tg.radius, "lightning_beam_wide", {tx=x-self.x, ty=y-self.y}) + game:shakeScreen(10, 3) + game:playSoundNear(self, "talents/reality_breach") return true end, info = function(self, t) - return ([[Using your sheer arcane power you keep breaches in spacetime open for %d turns. - Each turn you they are open you project an occult clone of your saw along each breach, damaging any creature caught for %d%% occult weapon damage. - The saw cuts both in a physical and arcane way, reducing the duration of a random beneficial effect on each target by %d each time. - Each target can only be affected once per turn. - This spell is only usable for one turn after casting Reality Breach but any Reality Breach cast during its duration is also recorded inside.]]) - :tformat(t.getDur(self, t), t.getDamage(self, t) * 100, t.getReduce(self, t)) + local damage = t.getDamage(self, t) + return ([[Using your near-perfect knowledge of beam spells you combine them all into a powerful 3-wide beam of pure energy. + The beam deals %0.2f thaumic damage and always goes as far as possible. + Thaumic damage can never be resisted by anything but "Resistance: All" and always uses your highest resistance penetration and highest damage type bonus. + It can trigger Burning Wake and Hurricane. + It is affected by the wet status. + It has a 25%% chance to either stun or freeze the targets for 3 turns (if Crystalline Focus or Uttercold are active, respectively). + Each time you deal damage with a beam spell, the remaining cooldown is reduced by 1 (this can happen only once per turn). + The damage will increase with your Spellpower.]]): + tformat(damDesc(self, DamageType.THAUM, damage)) end, } diff --git a/game/modules/tome/data/talents/spells/water.lua b/game/modules/tome/data/talents/spells/water.lua index 04d74e43f4ec34b2eb68a0f1044c13992e3bf1eb..b51b844ee22f81c4b52705769c474aeb25b8111b 100644 --- a/game/modules/tome/data/talents/spells/water.lua +++ b/game/modules/tome/data/talents/spells/water.lua @@ -27,10 +27,11 @@ newTalent{ tactical = { ATTACKAREA = { COLD = 1, stun = 1 } }, range = 10, radius = 1, + is_beam_spell = true, proj_speed = 8, requires_target = true, target = function(self, t) - if self:attr("archmage_widebeam") then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} + if thaumaturgyCheck(self) then return {type="widebeam", radius=1, range=self:getTalentRange(t), talent=t, selffire=false, friendlyfire=self:spellFriendlyFire()} else return {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), talent=t} end end, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 18, 200) end, @@ -39,7 +40,7 @@ newTalent{ local x, y = self:getTarget(tg) if not x or not y then return nil end - if self:attr("archmage_widebeam") then + if thaumaturgyCheck(self) then self:project(tg, x, y, DamageType.ICE, {chance=25, do_wet=true, dam=self:spellCrit(t.getDamage(self, t))}) game.level.map:particleEmitter(self.x, self.y, tg.radius, "ice_beam_wide", {tx=x-self.x, ty=y-self.y}) else diff --git a/game/modules/tome/data/talents/uber/mag.lua b/game/modules/tome/data/talents/uber/mag.lua index d072c624f4055cb3b3dfd225f99d67c86c822ff3..3d45b1570cf74064b02c9c7d6a055d94a1c42fa3 100644 --- a/game/modules/tome/data/talents/uber/mag.lua +++ b/game/modules/tome/data/talents/uber/mag.lua @@ -478,7 +478,8 @@ uberTalent{ - Orb of Thaumaturgy: a temporary orb that duplicates any beam spells that you cast - Multicaster: When casting a beam spell adds a chance to also cast an other archmage spell - Slipstream: Allows movement when casting beams - - Elemental Array Burst: a powerful, multi-elemental beam spell that can inflict all elemental ailments and can not be resisted]]) + - Elemental Array Burst: a powerful, multi-elemental beam spell that can inflict all elemental ailments and can not be resisted + #CRIMSON#The fine spellcasting required for wide beams and all thaumaturgy spells can only happen while wearing cloth. Anything heavier will hinder the casting too much.]]) :tformat() end, } diff --git a/game/modules/tome/data/timed_effects/magical.lua b/game/modules/tome/data/timed_effects/magical.lua index 1925a19b4865ca7a84cc1ec988274c4753d09318..5e1ac1ab6748a7d6489f19b94689fae5caaa952f 100644 --- a/game/modules/tome/data/timed_effects/magical.lua +++ b/game/modules/tome/data/timed_effects/magical.lua @@ -5301,3 +5301,58 @@ newEffect{ self:unlearnTalent(self.T_GHOST_WALK_RETURN, 1, nil, {no_unlearn=true}) end, } + +newEffect{ + name = "SLIPSTREAM", image = "talents/slipstream.png", + desc = _t"Slipstream Free Movement", + long_desc = function(self, eff) return _t"Can move once for free, this turn only." end, + type = "magical", + subtype = { thaumaturgy=true, movement=true }, + status = "beneficial", decrease = 0, + parameters = {}, + activate = function(self, eff) + self:effectTemporaryValue(eff, "free_movement", 1) + eff.setup = true + end, + callbackOnMove = function(self, eff, moved, force, ox, oy) + if not moved or force then return end + self:removeEffect(self.EFF_SLIPSTREAM, true, true) + self:callTalent(self.T_SLIPSTREAM, "useStream") + end, + callbackOnAct = function(self, eff) + if eff.setup then eff.setup = nil return end + self:removeEffect(self.EFF_SLIPSTREAM, true, true) + end, +} + +newEffect{ + name = "ORB_OF_THAUMATURGY", image = "talents/orb_of_thaumaturgy.png", + desc = _t"Orb Of Thaumaturgy", + long_desc = function(self, eff) return _t"All beam spells are duplicated to the orb." end, + type = "magical", + subtype = { thaumaturgy=true, meta=true }, + status = "beneficial", + parameters = {}, + callbackOnChangeLevel = function(self, eff, what) + if what ~= "leave" then return end + self:removeEffect(self.EFF_ORB_OF_THAUMATURGY, true, true) + end, + callbackOnTalentPost = function(self, eff, t) + if self._orb_of_thaumaturgy_recurs then return end + if not t.is_beam_spell then return end + game:onTickEnd(function() + local target = {x=eff.x, y=eff.y, __no_self=true} + self._orb_of_thaumaturgy_recurs = target + print("==============+HERE!!!!") + self:forceUseTalent(t.id, {ignore_cooldown=true, ignore_ressources=true, ignore_energy=true, force_target=target}) + print("==============+DONE!!!!") + self._orb_of_thaumaturgy_recurs = nil + end) + end, + activate = function(self, eff) + eff.particle = game.level.map:particleEmitter(eff.x, eff.y, 1, "corpselight", {}) + end, + deactivate = function(self, eff, ed) + game.level.map:removeParticleEmitter(eff.particle) + end, +} diff --git a/game/modules/tome/dialogs/LevelupDialog.lua b/game/modules/tome/dialogs/LevelupDialog.lua index 594db1590a0062a2f6bd0e2f265c1e35f175eee7..74b3fb3f93d546044bf8ae21f9deb3f218a3e834 100644 --- a/game/modules/tome/dialogs/LevelupDialog.lua +++ b/game/modules/tome/dialogs/LevelupDialog.lua @@ -324,7 +324,7 @@ function _M:checkDeps(simple, ignore_special) local t = self.actor:getTalentFromId(t_id) local ok, reason = self.actor:canLearnTalent(t, 0, ignore_special) - if not ok and (self.actor:knowTalent(t) or force) then talents = talents.."\n#GOLD##{bold}# - "..t.name.."#{normal}##LAST#("..reason..")" end + if not ok and (self.actor:knowTalent(t) or force) then talents = talents.."\n#GOLD##{bold}# - "..t.name.."#{normal}##LAST#("..(reason or _t"unknown")..")" end if reason == _t"not enough stat" then stats_ok = false end diff --git a/game/modules/tome/dialogs/UseItemDialog.lua b/game/modules/tome/dialogs/UseItemDialog.lua index 5db5105e7877ee906afd1f7b2e5c1a447c9b906a..a0821acb076ef900e2536242687478ca87b2a8f0 100644 --- a/game/modules/tome/dialogs/UseItemDialog.lua +++ b/game/modules/tome/dialogs/UseItemDialog.lua @@ -103,7 +103,11 @@ function _M:use(item) local list = {} for inven_idx, inven in pairs(self.actor.inven) do if inven.worn then for item_idx, o in ipairs(inven) do - if o:canAttachTinker(self.object, true) then list[#list+1] = {name=o:getName{do_color=true}, inven=inven, item=item_idx, o=o} end + if self.actor.tinker_restrict_slots and (not self.actor.tinker_restrict_slots[inven.name] or self.actor.tinker_restrict_slots[inven.name] < item_idx) then + -- nothing + else + if o:canAttachTinker(self.object, true) then list[#list+1] = {name=o:getName{do_color=true}, inven=inven, item=item_idx, o=o} end + end end end end local doit = function(w)