diff --git a/game/engines/default/engine/Game.lua b/game/engines/default/engine/Game.lua index 13b27922a7934cb783e375a7aa9ca4f145f0ddf8..789f72e08890d0277b891c7769d8ee84a9152dfc 100644 --- a/game/engines/default/engine/Game.lua +++ b/game/engines/default/engine/Game.lua @@ -50,9 +50,16 @@ function _M:init(keyhandler) end --- Log a message -function _M:log() end ---- Log something that was seen -function _M:logSeen() end +-- Redefine as needed +function _M.log(style, ...) end + +--- Log something associated with an entity that is seen by the player +-- Redefine as needed +function _M.logSeen(e, style, ...) end + +--- Log something associated with an entity if it is the player +-- Redefine as needed +function _M.logPlayer(e, style, ...) end --- Default mouse cursor function _M:defaultMouseCursor() @@ -270,9 +277,11 @@ end --- This is the "main game loop", do something here function _M:tick() - -- Check out any possible errors + -- If any errors have occurred, save them and open the error dialog local errs = core.game.checkError() if errs then + if not self.errors or self.errors.turn ~= self.turn then self.errors = {turn=self.turn, first_error = errs} end + self.errors.last_error = errs table.insert(self.errors, (#self.errors%10) + 1, errs) if config.settings.cheat then for id = #self.dialogs, 1, -1 do self:unregisterDialog(self.dialogs[id]) end end self:registerDialog(require("engine.dialogs.ShowErrorStack").new(errs)) end diff --git a/game/engines/default/engine/LogDisplay.lua b/game/engines/default/engine/LogDisplay.lua index 943f2482b3ff259d709b449edb02bbeb90f457e3..5c5bd2dbba1494514d8a8133876fd72f5a45969e 100644 --- a/game/engines/default/engine/LogDisplay.lua +++ b/game/engines/default/engine/LogDisplay.lua @@ -86,7 +86,7 @@ function _M:resize(x, y, w, h) self.mouse:registerZone(0, 0, self.w, self.h, function(button, x, y, xrel, yrel, bx, by, event) self:mouseEvent(button, x, y, xrel, yrel, bx, by, event) end) end ---- Returns the full log +--- Returns a clone of the full log function _M:getLog(extra, timestamp) local log = {} for i = 1, #self.log do @@ -140,9 +140,9 @@ function _M:call(str, ...) self.changed = true end ---- Gets the last log line +--- Gets the newest log line function _M:getNewestLine() - if self.log[1] then return self.log[1].str end + if self.log[1] then return self.log[1].str, self.log[1] end return nil end @@ -153,7 +153,35 @@ function _M:empty() self.changed = true end ---- Get Last Lines From Log +--- Remove some lines from the log, starting with the newest +-- @param line = number of lines to remove (default 1) or the last line (table, reference) to leave in the log +-- @param [type=table, optional] ret table in which to append removed lines +-- @param [type=number, optional] timestamp of the oldest line to remove +-- @return the table of removed lines or nil +function _M:rollback(line, ret, timestamp) + local nb = line or 1 + if type(line) == "table" then + nb = 0 + for i, ln in ipairs(self.log) do + if ln == line then nb = i - 1 break end + end + end + if nb > 0 then + for i = 1, nb do + local removed = self.log[1] + if not timestamp or removed.timestamp >= timestamp then + print("[LOG][remove]", removed.timestamp, removed.str) + table.remove(self.log, 1) + if ret then ret[#ret+1] = removed end + else break + end + end + self.changed = true + end + return ret +end + +--- Get the oldest lines from the log -- @param number number of lines to retrieve function _M:getLines(number) local from = number diff --git a/game/engines/default/engine/dialogs/ShowErrorStack.lua b/game/engines/default/engine/dialogs/ShowErrorStack.lua index a8f3b90ddce2649c55dd63cfa3696e3af7d52b83..7a22e45e0e841a75c2f642e86f1215957230889d 100644 --- a/game/engines/default/engine/dialogs/ShowErrorStack.lua +++ b/game/engines/default/engine/dialogs/ShowErrorStack.lua @@ -94,6 +94,12 @@ If you are not currently connected to the internet, please report this bug when self.key:addBinds{ EXIT = function() game:unregisterDialog(self) end, + LUA_CONSOLE = function() + if config.settings.cheat then + local DebugConsole = require "engine.DebugConsole" + game:registerDialog(DebugConsole.new()) + end + end, } end diff --git a/game/engines/default/engine/interface/ActorLife.lua b/game/engines/default/engine/interface/ActorLife.lua index c7b6f25d218c5d346be12d0da5a4070e23a25de0..3e7c8b74d9a6f4fea1064c8e57f225ee3829df9e 100644 --- a/game/engines/default/engine/interface/ActorLife.lua +++ b/game/engines/default/engine/interface/ActorLife.lua @@ -95,7 +95,7 @@ end --- Actor is being attacked! -- Module authors should rewrite it to handle combat, dialog, ... --- @param target the actor attacking us +-- @param target the actor being attacked -- @param x placeholder -- @param y placeholder function _M:attack(target, x, y) diff --git a/game/engines/default/engine/interface/ActorTalents.lua b/game/engines/default/engine/interface/ActorTalents.lua index 702fa8bec56905ff31f55e37d91d862f234772ec..9bab416fad8b8217bbdedb8b734e926859243fc9 100644 --- a/game/engines/default/engine/interface/ActorTalents.lua +++ b/game/engines/default/engine/interface/ActorTalents.lua @@ -117,7 +117,7 @@ function _M:resolveLevelTalents() end end --- Make the actor use the talent +--- Make the actor use a talent -- @param id talent ID -- @param who talent user -- @param force_level talent level(raw) override @@ -125,52 +125,80 @@ end -- @param force_target the target of the talent (override) -- @param silent do not display messages about use -- @param no_confirm Never ask confirmation +-- @return the return value from the talent code if successful or false: +-- activated talents: value returned from the action function +-- sustainable talents: value returned from the activate function (if inactive, should be a table of parameters) +-- or value returned from the talent deactivate function (if active) +-- talent code should always return a non false/nil result if successful function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_confirm) who = who or self - local ab = _M.talents_def[id] - assert(ab, "trying to cast talent "..tostring(id).." but it is not defined") + local ab, ret = _M.talents_def[id] + assert(ab, "trying to use talent "..tostring(id).." but it is not defined") + self.talent_error = nil local cancel = false local co, success, err + + local msg, line + if ab.mode == "activated" and ab.action then if self:isTalentCoolingDown(ab) and not ignore_cd then game.logPlayer(who, "%s is still on cooldown for %d turns.", ab.name:capitalize(), self.talents_cd[ab.id]) - return + return false end - co = coroutine.create(function() + co = coroutine.create(function() -- coroutine to run activated talent code if cancel then success = false return false end - if not self:preUseTalent(ab, silent) then return end + + if not self:preUseTalent(ab, silent) then return false end + + msg, line = game.logNewest() + if not silent then self:logTalentMessage(ab) end local ok, ret, special = xpcall(function() return ab.action(who, ab) end, debug.traceback) self.__talent_running = nil + if not ok then self:onTalentLuaError(ab, ret) error(ret) end - if not ok then error(ret) end + if not ret and self._silent_talent_failure then -- remove messages generated by failed talent + local msg, newline = game.logNewest() + if newline ~= line then game.logRollback(line) end + end - if not self:postUseTalent(ab, ret, silent) then return end + if not self:postUseTalent(ab, ret, silent) then return false end -- Everything went ok? then start cooldown if any if not ignore_cd and (not special or not special.ignore_cd) then self:startTalentCooldown(ab) end + return ret end) elseif ab.mode == "sustained" and ab.activate and ab.deactivate then if self:isTalentCoolingDown(ab) and not ignore_cd then game.logPlayer(who, "%s is still on cooldown for %d turns.", ab.name:capitalize(), self.talents_cd[ab.id]) - return + return false end - co = coroutine.create(function() + co = coroutine.create(function() -- coroutine to run sustainable talent code if cancel then success = false return false end - if not self:preUseTalent(ab, silent) then return end - if not self.sustain_talents[id] then - local ret = ab.activate(who, ab) + if not self:preUseTalent(ab, silent) then return false end + msg, line = game.logNewest() + if not silent then self:logTalentMessage(ab) end + + local ok, ret, special + if not self.sustain_talents[id] then -- activating + ok, ret, special = xpcall(function() return ab.activate(who, ab) end, debug.traceback) + if not ok then self:onTalentLuaError(ab, ret) error(ret) end if ret == true then ret = {} end -- fix for badly coded talents if ret then ret.name = ret.name or ab.name end - if not self:postUseTalent(ab, ret) then return end + if not ret and self._silent_talent_failure then -- remove messages generated by failed talent + local msg, newline = game.logNewest() + if newline ~= line then game.logRollback(line) end + end + + if not self:postUseTalent(ab, ret, silent) then return false end self.sustain_talents[id] = ret @@ -186,7 +214,7 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_ table.insert(list, id) end end - else + else -- deactivating if self.deactivating_sustain_talent == ab.id then return end local p = self.sustain_talents[id] @@ -202,10 +230,16 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_ end end p.__tmpparticles = nil - local ret = ab.deactivate(who, ab, p) + ok, ret, special = xpcall(function() return ab.deactivate(who, ab, p) end, debug.traceback) + if not ok then self:onTalentLuaError(ab, ret) error(ret) end + + if not ret and self._silent_talent_failure then -- remove messages generated by failed talent + local msg, newline = game.logNewest() + if newline ~= line then game.logRollback(line) end + end self.deactivating_sustain_talent = ab.id - if not self:postUseTalent(ab, ret, silent) then self.deactivating_sustain_talent = nil return end + if not self:postUseTalent(ab, ret, silent) then self.deactivating_sustain_talent = nil return false end self.deactivating_sustain_talent = nil -- Everything went ok? then start cooldown if any @@ -225,11 +259,12 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_ end end end + return ret end) else - print("Activating non activable or sustainable talent: "..id.." :: "..ab.name.." :: "..ab.mode) + print("[useTalent] Attempt to use non activated or sustainable talent: "..id.." :: "..ab.name.." :: "..ab.mode) end - if co then + if co then -- talent is usable and has passed checks -- Stub some stuff local old_level, old_target, new_target = nil, nil, nil if force_level then old_level = who.talents[id] end @@ -237,23 +272,27 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_ if ab.onAIGetTarget and not who.player then old_target = rawget(who, "getTarget"); new_target = function() return ab.onAIGetTarget(self, ab) end end if force_target and not old_target then old_target = rawget(who, "getTarget"); new_target = function(a) return force_target.x, force_target.y, not force_target.__no_self and force_target end end end - local co_wrapper = coroutine.create(function() + + local co_wrapper = coroutine.create(function() -- coroutine for talent interface success = true local ok while success do if new_target then who.getTarget = new_target end if force_level then who.talents[id] = force_level end self.__talent_running = ab - ok, err = coroutine.resume(co) + ok, ret = coroutine.resume(co) -- ret == error or return value from co success = success and ok if new_target then who.getTarget = old_target end if force_level then who.talents[id] = old_level end self.__talent_running = nil - if ok and coroutine.status(co) == "dead" then - -- terminated - return + if ok and coroutine.status(co) == "dead" then -- coroutine terminated normally + if success and not ret then -- talent failed + print("[useTalent] TALENT FAILED:", ab.id, "for", self.name, self.uid, success) + --game.log("#ORANGE# %s TALENT USE FAILED [%s (silent_failure:%s at (%s, %s)]", ab.id, self.name, self._silent_talent_failure, self.x, self.y) -- debugging + end + return ret end - if err then error(err) end --propagate + if ret then error(ret) end --propagate error coroutine.yield() end end) @@ -264,27 +303,35 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_ if quit ~= false then cancel = true end - success, err = coroutine.resume(co_wrapper) + success, ret = coroutine.resume(co_wrapper) + if not success and ret then -- talent code error + self:onTalentLuaError(ab, ret) + --print("useTalent:", debug.traceback(co_wrapper), '\n') + error(ret) + end end, "Cancel","Continue") else - -- cancel checked in coroutine - success, err = coroutine.resume(co_wrapper) + success, ret = coroutine.resume(co_wrapper) -- cancel checked in coroutine end -- Cleanup in case we coroutine'd out self.__talent_running = nil - if not success and err then - print(debug.traceback(co_wrapper)) - self:onTalentLuaError(err) - error(err) + if not success and ret then -- talent code error + self:onTalentLuaError(ab, ret) + --print("useTalent:", debug.traceback(co_wrapper), '\n') + error(ret) end end self.changed = true - return true - + + return ret -- return value from successfully used talent end ---- Replace some markers in a string with info on the talent +--- Set true to remove game log messages generated by talents that started but did not complete +-- affects messages logged after preUseTalent check when action/activate/deactivate function returns nil or false +_M._silent_talent_failure = false + +--- Get the talent use message, replacing some markers in its message string with info on the talent function _M:useTalentMessage(ab) if not ab.message then return nil end local str = util.getval(ab.message, self, ab) @@ -299,32 +346,56 @@ function _M:useTalentMessage(ab) return str end ---- Called before a talent is used +--- Display the talent use message in the game log -- Redefine as needed +-- called in useTalent after successful preUseTalent check -- @param[type=table] talent the talent (not the id, the table) +-- uses ab.message if defined or generates default use text (ab.message == false suppresses) +function _M:logTalentMessage(ab) + if ab.message == false then return + elseif ab.message then + game.logSeen(self, "%s", self:useTalentMessage(ab)) + elseif ab.mode == "sustained" then + game.logSeen(self, "%s %s %s.", self.name:capitalize(), self:isTalentActive(ab.id) and "deactivates" or "activates", ab.name) + else + game.logSeen(self, "%s uses %s.", self.name:capitalize(), ab.name) + end +end + +--- Called BEFORE a talent is used -- CAN it be used? +-- Redefine as needed +-- @param[type=table] ab the talent (not the id, the table) -- @param[type=boolean] silent no messages will be outputted -- @param[type=boolean] fake no actions are taken, only checks -- @return[1] true to continue -- @return[2] false to stop -function _M:preUseTalent(talent, silent, fake) +function _M:preUseTalent(ab, silent, fake) return true end ---- Called before a talent is used +--- Called AFTER a talent is used -- WAS it successfully used? -- Redefine as needed --- @param[type=table] talent the talent (not the id, the table) --- @param ret the return of the talent action +-- @param[type=table] ab the talent (not the id, the table) +-- @param ret the return of the talent action, activate, or deactivate function -- @param[type=boolean] silent no messages will be outputted -- @return[1] true to continue -- @return[2] false to stop -function _M:postUseTalent(talent, ret, silent) +function _M:postUseTalent(ab, ret, silent) return true end ---- Called if a talent errors out --- @param ab the talent --- @param err the error +--- Called if a talent errors out when used +-- Redefine as needed +-- @param ab the talent table +-- @param err the table of errors returned from xpcall +-- sets self.talent_error and logs errors to _M._talent_errors +-- data forma: {[ab.id]=ab, Actor=self, uid=, x=, y=, err=err, turn=game.turn} function _M:onTalentLuaError(ab, err) + if self.talent_error then return end -- handle only the first error + self.talent_error = {[ab.id]=ab, Actor=self, uid=self.uid, name=self.name, err=err, x=self.x, y=self.y, turn=game.turn} + print("##Use Talent Lua Error##", ab and ab.id, "Actor:", self.uid, self.name) + _M._talent_errors = _M._talent_errors or {} -- log the error globally + table.insert(_M._talent_errors, self.talent_error) return end @@ -552,7 +623,7 @@ function _M:updateTalentPassives(tid) t.passives(self, t, self.talents_learn_vals[t.id]) end ---- Checks the talent if learnable +--- Checks if the talent can be learned -- @param t the talent to check -- @param offset the level offset to check, defaults to 1 -- @param ignore_special ignore requirement of special @@ -691,7 +762,7 @@ function _M:getTalentLevelRaw(id) end --- Talent level, 0 if not known --- Includes mastery +-- Includes mastery (defaults to 1) function _M:getTalentLevel(id) local t @@ -700,7 +771,7 @@ function _M:getTalentLevel(id) else t = _M.talents_def[id] end - return (self:getTalentLevelRaw(id)) * ((self.talents_types_mastery[t.type[1]] or 0) + 1) + return t and (self:getTalentLevelRaw(id)) * ((self.talents_types_mastery[t.type[1]] or 0) + 1) or 0 end --- Talent type level, sum of all raw levels of talents inside @@ -823,21 +894,21 @@ function _M:isTalentCoolingDown(t) if self.talents_cd[t.id] and self.talents_cd[t.id] > 0 then return self.talents_cd[t.id] else return false end end ---- Returns the range of a talent +--- Returns the range of a talent (defaults to 1) function _M:getTalentRange(t) if not t.range then return 1 end if type(t.range) == "function" then return t.range(self, t) end return t.range end ---- Returns the radius of a talent +--- Returns the radius of a talent (defaults to 0) function _M:getTalentRadius(t) if not t.radius then return 0 end if type(t.radius) == "function" then return t.radius(self, t) end return t.radius end ---- Returns the target type of a talent +--- Returns the target table for a talent function _M:getTalentTarget(t) if type(t.target) == "function" then return t.target(self, t) end return t.target @@ -992,7 +1063,7 @@ function _M:talentDialog(d) local ok, err = coroutine.resume(co, dialog_returns[d]) if not ok and err then print(debug.traceback(co)) - self:onTalentLuaError(err) + self:onTalentLuaError(nil, err) error(err) end end diff --git a/game/engines/default/modules/boot/class/Game.lua b/game/engines/default/modules/boot/class/Game.lua index 5baf53803acd39247e360066988dd3243dd66070..9bee703bb3e7df04971be40282cb18f1e32fa197 100644 --- a/game/engines/default/modules/boot/class/Game.lua +++ b/game/engines/default/modules/boot/class/Game.lua @@ -124,6 +124,8 @@ function _M:run() self.log = function(style, ...) end self.logSeen = function(e, style, ...) end self.logPlayer = function(e, style, ...) end + self.logRollback = function(line, ...) return self.logdisplay:rollback(line, ...) end + self.logNewest = function() return self.logdisplay:getNewestLine() end self.nicer_tiles = NicerTiles.new() -- Starting from here we create a new game diff --git a/game/modules/tome/ai/tactical.lua b/game/modules/tome/ai/tactical.lua index 716e9be2a739de77e99dfc490f1165f3f6a2fac7..b6cf8ad7257696750c498160e59b22d66eee8057 100644 --- a/game/modules/tome/ai/tactical.lua +++ b/game/modules/tome/ai/tactical.lua @@ -469,6 +469,7 @@ newAI("tactical", function(self) self.energy.used = true self.ai_state.last_tid = used_talent end + return true end return false end) diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua index 1c497be76287cff7d13936313e3c715142a85022..2668018e5630ef54c6cd412ef11416d1bfb9b8f4 100644 --- a/game/modules/tome/class/Actor.lua +++ b/game/modules/tome/class/Actor.lua @@ -3434,8 +3434,11 @@ function _M:onTemporaryValueChange(prop, v, base) end end +--- Actor attacks target +-- @param target the actor being attacked +-- @param x, y coordinates of target grid function _M:attack(target, x, y) - self:bumpInto(target, x, y) + return self:bumpInto(target, x, y) end function _M:getMaxEncumbrance() @@ -4891,24 +4894,24 @@ function _M:preUseTalent(ab, silent, fake) -- Special checks -- AI if not self.player and ab.on_pre_use_ai and not (ab.mode == "sustained" and self:isTalentActive(ab.id)) and not ab.on_pre_use_ai(self, ab, silent, fake) then return false end - if not silent then - -- Allow for silent talents - if ab.message ~= nil then - if ab.message then - game.logSeen(self, "%s", self:useTalentMessage(ab)) - end - elseif ab.mode == "sustained" and not self:isTalentActive(ab.id) then - game.logSeen(self, "%s activates %s.", self.name:capitalize(), ab.name) - elseif ab.mode == "sustained" and self:isTalentActive(ab.id) then - game.logSeen(self, "%s deactivates %s.", self.name:capitalize(), ab.name) + return true +end + +--- Display the talent use message in the game log +-- called when the talent is used after successful preUseTalent check +-- @param ab the talent (not the id, the table) +function _M:logTalentMessage(ab) + if ab.message ~= false and not util.getval(ab.no_message, self, ab) then + if ab.message then + game.logSeen(self, "%s", self:useTalentMessage(ab)) + elseif ab.mode == "sustained" then + game.logSeen(self, "%s %s %s.", self.name:capitalize(), self:isTalentActive(ab.id) and "deactivates" or "activates", ab.name) elseif ab.is_spell then game.logSeen(self, "%s casts %s.", self.name:capitalize(), ab.name) - elseif not ab.no_message then + else game.logSeen(self, "%s uses %s.", self.name:capitalize(), ab.name) end end - - return true end local sustainCallbackCheck = { diff --git a/game/modules/tome/class/NPC.lua b/game/modules/tome/class/NPC.lua index 9cce3305f9d2aa6904ccaf8cde3a85ad942bf7fb..e02fb5cfd59039dd57b08dc7cf1bece0df1e4696 100644 --- a/game/modules/tome/class/NPC.lua +++ b/game/modules/tome/class/NPC.lua @@ -34,6 +34,8 @@ function _M:init(t, no_default) if not self.image and self.name ~= "unknown actor" then self.image = "npc/"..tostring(self.type or "unknown").."_"..tostring(self.subtype or "unknown"):lower():gsub("[^a-z0-9]", "_").."_"..(self.name or "unknown"):lower():gsub("[^a-z0-9]", "_")..".png" end end +_M._silent_talent_failure = true + function _M:actBase() -- Reduce shoving pressure every turn if self.shove_pressure then @@ -109,6 +111,7 @@ local function spotHostiles(self) end function _M:onTalentLuaError(ab, err) + engine.interface.ActorTalents.onTalentLuaError(self, ab, err) self:useEnergy() -- prevent infinitely long erroring out turns end diff --git a/game/modules/tome/class/interface/ActorObjectUse.lua b/game/modules/tome/class/interface/ActorObjectUse.lua index 1ae770a2c94fc881a285968d8320082808edc164..4bd05d95c6c93cd02de5dea832f664b8134e265f 100644 --- a/game/modules/tome/class/interface/ActorObjectUse.lua +++ b/game/modules/tome/class/interface/ActorObjectUse.lua @@ -225,6 +225,7 @@ _M.useObjectBaseTalent ={ end end) coroutine.resume(co) + return ret and ret.used end, info = function(self, t) local o = t.getObject(self, t) diff --git a/game/modules/tome/class/interface/Combat.lua b/game/modules/tome/class/interface/Combat.lua index bba2c0102af79c96830c203b5cafcec1e63aec1b..654b8ac2396947074a39a3e13bfc384552e6a9d9 100644 --- a/game/modules/tome/class/interface/Combat.lua +++ b/game/modules/tome/class/interface/Combat.lua @@ -31,10 +31,10 @@ module(..., package.seeall, class.make) -- Talk ? attack ? displace ? function _M:bumpInto(target, x, y) local reaction = self:reactionToward(target) - if reaction < 0 then + if reaction < 0 then -- attack target if possible if target.encounterAttack and self.player then self:onWorldEncounter(target, x, y) return end if game.player == self and ((not config.settings.tome.actor_based_movement_mode and game.bump_attack_disabled) or (config.settings.tome.actor_based_movement_mode and self.bump_attack_disabled)) then return end - return self:useTalent(self.T_ATTACK, nil, nil, nil, target) + return self:enoughEnergy(game.energy_to_act*self:combatSpeed()) and self:useTalent(self.T_ATTACK, nil, nil, nil, target) elseif reaction >= 0 then -- Talk ? Bump ? if self.player and target.on_bump then diff --git a/game/modules/tome/class/uiset/Classic.lua b/game/modules/tome/class/uiset/Classic.lua index a2ecf284d458b009a6a13a2d4b17a3639a85afa9..ab75975570e040814ff2df24eff24b3374ba06ff 100644 --- a/game/modules/tome/class/uiset/Classic.lua +++ b/game/modules/tome/class/uiset/Classic.lua @@ -92,6 +92,8 @@ function _M:activate() game.uiset.logdisplay(...) else game.uiset.logdisplay(style, ...) end if game.uiset.show_userchat then game.uiset.logdisplay.changed = old end end + game.logRollback = function(line, ...) return self.logdisplay:rollback(line, ...) end + game.logNewest = function() return self.logdisplay:getNewestLine() end -- game.logSeen = function(e, style, ...) if e and e.player or (not e.dead and e.x and e.y and game.level and game.level.map.seens(e.x, e.y) and game.player:canSee(e)) then game.log(style, ...) end end game.logPlayer = function(e, style, ...) if e == game.player or e == game.party then game.log(style, ...) end end end diff --git a/game/modules/tome/class/uiset/Minimalist.lua b/game/modules/tome/class/uiset/Minimalist.lua index 669f66cfd6d90f0bace4b9897bfb36cf0fb9bd9f..4bc3702d08a03570b44490cca39fcd32e62cb4a5 100644 --- a/game/modules/tome/class/uiset/Minimalist.lua +++ b/game/modules/tome/class/uiset/Minimalist.lua @@ -490,6 +490,8 @@ function _M:activate() game.uiset.logdisplay(...) else game.uiset.logdisplay(style, ...) end if game.uiset.show_userchat then game.uiset.logdisplay.changed = old end end + game.logRollback = function(line, ...) return self.logdisplay:rollback(line, ...) end + game.logNewest = function() return self.logdisplay:getNewestLine() end -- game.logSeen = function(e, style, ...) if e and e.player or (not e.dead and e.x and e.y and game.level and game.level.map.seens(e.x, e.y) and game.player:canSee(e)) then game.log(style, ...) end end game.logPlayer = function(e, style, ...) if e == game.player or e == game.party then game.log(style, ...) end end diff --git a/game/modules/tome/data/chats/artifice-mastery.lua b/game/modules/tome/data/chats/artifice-mastery.lua index f77068676cc95cdc7176c748bbf0f9727abfb7fe..d7acb1f57b681b681da2f98c4da24029b52ea948 100644 --- a/game/modules/tome/data/chats/artifice-mastery.lua +++ b/game/modules/tome/data/chats/artifice-mastery.lua @@ -18,40 +18,42 @@ -- darkgod@te4.org local Talents = require("engine.interface.ActorTalents") - +chat_talent = player:getTalentFromId(chat_tid) +chat_level = player:getTalentLevelRaw(chat_tid) local function generate_tools() - local answers = {} - local tools = - { - } - - if player:knowTalent(player.T_HIDDEN_BLADES) then tools[Talents.T_ASSASSINATE] = 1 end - if player:knowTalent(player.T_SMOKESCREEN) then tools[Talents.T_SMOKESCREEN_MASTERY] = 1 end - if player:knowTalent(player.T_ROGUE_S_BREW) then tools[Talents.T_ROGUE_S_BREW_MASTERY] = 1 end - if player:knowTalent(player.T_DART_LAUNCHER) then tools[Talents.T_DART_LAUNCHER_MASTERY] = 1 end + local answers = {{"Cancel"}} + for tid, m_tid in pairs(tool_ids) do + local t = player:getTalentFromId(tid) + local m_t = player:getTalentFromId(m_tid) + if m_t then + local master_talent = function(npc, player) + local old_mastery_level = player:getTalentLevelRaw(m_tid) + if old_mastery_level == chat_level then return end + -- unlearn mastery talent(s) + for tid, m_tid in pairs(tool_ids) do + if player:knowTalent(m_tid) then player:unlearnTalentFull(m_tid) end + end + + player:learnTalent(m_tid, true, chat_level, {no_unlearn=true}) + player.artifice_tools_mastery = tid + + -- start talent cooldowns + if old_mastery_level == 0 then + player:startTalentCooldown(tid) player:startTalentCooldown(m_tid) + player:startTalentCooldown(chat_tid) + game.log("#LIGHT_BLUE# You enhance your preparation of %s.", t.name) + end - - if tools then - for tid, level in pairs(tools) do - local t = npc:getTalentFromId(tid) - level = math.min(t.points - game.player:getTalentLevelRaw(tid), level) - - local doit = function(npc, player) - if game.player:knowTalentType(t.type[1]) == nil then player:setTalentTypeMastery(t.type[1], 1.0) end - player:learnTalent(tid, true, level, {no_unlearn=true}) - player:startTalentCooldown(tid) end - answers[#answers+1] = {("[%s]"):format(t.name), - action=doit, + answers[#answers+1] = {("%s[%s -- mastery: %s]#LAST#"):format(player.artifice_tools_mastery == tid and "#YELLOW#" or "", t.name, m_t.name), + action=master_talent, on_select=function(npc, player) local mastery = nil - if player:knowTalentType(t.type[1]) == nil then mastery = 1.0 end game.tooltip_x, game.tooltip_y = 1, 1 - game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..t.name.."#LAST#\n"..tostring(player:getTalentFullDescription(t, 1, nil, mastery))) + game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..m_t.name.."#LAST#\n"..tostring(player:getTalentFullDescription(m_t, nil, {force_level=chat_level}, mastery))) end, } end - answers[#answers+1] = {"Cancel"} end return answers end diff --git a/game/modules/tome/data/chats/artifice.lua b/game/modules/tome/data/chats/artifice.lua index 5587262325070b290a359f5ea7f6a61896f476cc..0ab43a18dec43ab302b6c4d7af8cd822cd921f08 100644 --- a/game/modules/tome/data/chats/artifice.lua +++ b/game/modules/tome/data/chats/artifice.lua @@ -18,63 +18,80 @@ -- darkgod@te4.org local Talents = require("engine.interface.ActorTalents") +chat_talent = player:getTalentFromId(chat_tid) +chat_level = player:getTalentLevelRaw(chat_tid) local function generate_tools() - local answers = {} - local tools = - { - } - - --populate the tool list, also apply a temp value so talents display correctly - if not player.artifice_hidden_blades then - tools[Talents.T_HIDDEN_BLADES] = 1 - player.artifice_hidden_blades = slot - end - if not player.artifice_smokescreen then - tools[Talents.T_SMOKESCREEN] = 1 - player.artifice_smokescreen = slot - end - if not player.artifice_rogue_s_brew then - tools[Talents.T_ROGUE_S_BREW] = 1 - player.artifice_rogue_s_brew = slot - end - if not player.artifice_dart_launcher then - tools[Talents.T_DART_LAUNCHER] = 1 - player.artifice_dart_launcher = slot - end + local answers = {{"[Cancel]"}} + local tool_ids = tool_ids or player.main_env.artifice_tool_tids + player.artifice_tools = player.artifice_tools or {} - if tools then - for tid, level in pairs(tools) do - local t = npc:getTalentFromId(tid) - level = math.min(t.points - game.player:getTalentLevelRaw(tid), level) - - local doit = function(npc, player) - if game.player:knowTalentType(t.type[1]) == nil then player:setTalentTypeMastery(t.type[1], 1.0) end - player:learnTalent(tid, true, level, {no_unlearn=true}) - --remove the temp values set earlier - if not (t.name=="Hidden Blades" or player:knowTalent(player.T_HIDDEN_BLADES)) then player.artifice_hidden_blades = null end - if not (t.name=="Smokescreen" or player:knowTalent(player.T_SMOKESCREEN)) then player.artifice_smokescreen = null end - if not (t.name=="Rogue's Brew" or player:knowTalent(player.T_ROGUE_S_BREW)) then player.artifice_rogue_s_brew = null end - if not (t.name=="Dart Launcher" or player:knowTalent(player.T_DART_LAUNCHER)) then player.artifice_dart_launcher = null end + for tid, m_tid in pairs(tool_ids) do + local t = player:getTalentFromId(tid) + if t then + + local tool_level = player:getTalentLevelRaw(t) + local equip_tool = function(npc, player) -- equip a tool + if tool_level == chat_level then return end -- already selected and up to date + -- unlearn the previous talent + player:unlearnTalentFull(player.artifice_tools[chat_tid]) + -- (re)learn the talent + player:unlearnTalentFull(tid) + player:learnTalent(tid, true, chat_level, {no_unlearn=true}) + -- clear other tool slots + for slot, tool_id in pairs(player.artifice_tools) do + if tool_id == tid then player.artifice_tools[slot] = nil end + end + player.artifice_tools[chat_tid] = tid + + -- start talent cooldowns and use energy + player.turn_procs._did_artifice = true -- controls energy use player:startTalentCooldown(tid) + if player:getTalentFromId(m_tid) then player:startTalentCooldown(m_tid) end end - answers[#answers+1] = {("[Equip %s]"):format(t.name), - action=doit, + local txt, slot + -- check for an existing slot + for slot_id, tool_id in pairs(player.artifice_tools) do + if tool_id == tid then slot = slot_id break end + end + if slot then + txt = ("[%sEquip %s%s#LAST#]"):format(slot==chat_tid and "#YELLOW#" or "", t.name, slot and (" (%s)"):format(player:getTalentFromId(slot).name) or "") + else + txt = ("[Equip %s]"):format(t.name) + end + + answers[#answers+1] = {txt, + action=equip_tool, on_select=function(npc, player) - local mastery = nil - if player:knowTalentType(t.type[1]) == nil then mastery = 1.0 end + local display_level + display_level = chat_level - tool_level game.tooltip_x, game.tooltip_y = 1, 1 - game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..t.name.."#LAST#\n"..tostring(player:getTalentFullDescription(t, 1, nil, mastery))) + + -- set up tooltip + local text = tstring{} + if display_level ~= 0 and player:knowTalent(t) then + local diff = function(i2, i1, res) + if i2 > i1 then + res:add({"color", "LIGHT_GREEN"}, i1, {"color", "LAST"}, " [->", {"color", "YELLOW_GREEN"}, i2, {"color", "LAST"}, "]") + elseif i2 < i1 then + res:add({"color", "LIGHT_GREEN"}, i1, {"color", "LAST"}, " [->", {"color", "LIGHT_RED"}, i2, {"color", "LAST"}, "]") + end + end + text:merge(player:getTalentFullDescription(t, display_level, nil):diffWith(player:getTalentFullDescription(t, 0, nil), diff)) + else + text = player:getTalentFullDescription(t, nil, {force_level=chat_level}) + end + game:tooltipDisplayAtMap(game.w, game.h, "#GOLD#"..t.name.."#LAST#\n"..tostring(text)) end, } end - end + return answers end newChat{ id="welcome", - text = [[Equip which tools?]], + text = ([[Equip which tool for #YELLOW#%s#LAST#?]]):format(chat_talent.name), answers = generate_tools(), } diff --git a/game/modules/tome/data/damage_types.lua b/game/modules/tome/data/damage_types.lua index b8cd0bd78536c5ef0716bbb4aa8454e6c83536ef..795227a7adb551582c54318a9b233a78deffda27 100644 --- a/game/modules/tome/data/damage_types.lua +++ b/game/modules/tome/data/damage_types.lua @@ -239,7 +239,7 @@ setDefaultProjector(function(src, x, y, type, dam, state) end end - if src and dam > 0 and src.knowTalent and src:knowTalent(src.T_BACKSTAB) and src.__CLASSNAME ~= "mod.class.Grid" then + if dam > 0 and src and src.__is_actor and src:knowTalent(src.T_BACKSTAB) and src.__CLASSNAME ~= "mod.class.Grid" then local power = src:callTalent("T_BACKSTAB", "getDamageBoost") local nb = 0 for eff_id, p in pairs(target.tmp) do @@ -3878,11 +3878,13 @@ newDamageType{ newDamageType{ name = "terror", type = "TERROR", + text_color = "#YELLOW#", projector = function(src, x, y, type, dam, state) state = initState(state) useImplicitCrit(src, state) local target = game.level.map(x, y, Map.ACTOR) if target then + game:delayedLogDamage(src, target, 0, ("%s<terror chance>#LAST#"):format(DamageType:get(type).text_color or "#aaaaaa#"), false) if not src:checkHit(src:combatAttack(), target:combatMentalResist()) then return end local effect = rng.range(1, 3) if effect == 1 then @@ -3908,6 +3910,7 @@ newDamageType{ end, } +-- Random poison: 25% to be enhanced newDamageType{ name = "random poison", type = "RANDOM_POISON", text_color = "#LIGHT_GREEN#", projector = function(src, x, y, t, dam, poison, state) @@ -3915,10 +3918,10 @@ newDamageType{ useImplicitCrit(src, state) local power local target = game.level.map(x, y, Map.ACTOR) - if target and src:reactionToward(target) < 0 then + if target and src:reactionToward(target) < 0 and target:canBe("poison") then local realdam = DamageType:get(DamageType.NATURE).projector(src, x, y, DamageType.NATURE, dam.dam / 6, state) - chance = rng.range(1, 3) - if target and target:canBe("poison") and rng.percent(25) then + if rng.percent(dam.random_chance or 25) then + local chance = rng.range(1, 3) if chance == 1 then target:setEffect(target.EFF_INSIDIOUS_POISON, 5, {src=src, power=dam.dam / 6, heal_factor=dam.power*2, apply_power=dam.apply_power or (src.combatAttack and src:combatAttack()) or 0}) elseif chance == 2 then @@ -3926,7 +3929,7 @@ newDamageType{ elseif chance == 3 then target:setEffect(target.EFF_CRIPPLING_POISON, 5, {src=src, power=dam.dam / 6, fail=dam.power, apply_power=dam.apply_power or (src.combatAttack and src:combatAttack()) or 0}) end - elseif target and target:canBe("poison") then + else target:setEffect(target.EFF_POISONED, 5, {src=src, power=dam.dam / 6, apply_power=dam.apply_power or (src.combatAttack and src:combatAttack()) or 0}) end return realdam @@ -3936,11 +3939,13 @@ newDamageType{ newDamageType{ name = "blinding powder", type = "BLINDING_POWDER", + text_color = "#GREY#", projector = function(src, x, y, type, dam, state) state = initState(state) useImplicitCrit(src, state) local target = game.level.map(x, y, Map.ACTOR) if target then + game:delayedLogDamage(src, target, 0, ("%s<blinding powder>#LAST#"):format(DamageType:get(type).text_color or "#aaaaaa#"), false) if not src:checkHit(src:combatAttack(), target:combatPhysicalResist()) then return end if target:canBe("blind") then @@ -3955,12 +3960,13 @@ newDamageType{ newDamageType{ name = "smokescreen", type = "SMOKESCREEN", + text_color = "#GREY#", projector = function(src, x, y, type, dam, state) state = initState(state) useImplicitCrit(src, state) local target = game.level.map(x, y, Map.ACTOR) if target and src:reactionToward(target) < 0 then - + game:delayedLogDamage(src, target, 0, ("%s<smoke>#LAST#"):format(DamageType:get(type).text_color or "#aaaaaa#"), false) if target:canBe("blind") then target:setEffect(target.EFF_DIM_VISION, 2, {sight=dam.dam, apply_power=src:combatAttack(), no_ct_effect=true}) else diff --git a/game/modules/tome/data/general/objects/world-artifacts.lua b/game/modules/tome/data/general/objects/world-artifacts.lua index a75efb047cf17ac81c25ea970d2e8322d27235dc..1a0d13ba109ad73bb865df1cac56174fd18407a3 100644 --- a/game/modules/tome/data/general/objects/world-artifacts.lua +++ b/game/modules/tome/data/general/objects/world-artifacts.lua @@ -461,7 +461,7 @@ newEntity{ base = "BASE_SHIELD", fatigue = 20, learn_talent = { [Talents.T_BLOCK] = 5, }, }, - on_block = {desc = "30% chance that you'll breath stunning fire on foes in a 6 radius cone.", fct = function(self, who, target, type, dam, eff) + on_block = {desc = "30% chance that you'll breath stunning fire in a cone at the attacker (if within range 6).", fct = function(self, who, target, type, dam, eff) if rng.percent(30) then if not target or not target.x or not target.y or core.fov.distance(who.x, who.y, target.x, target.y) > 6 then return end @@ -531,18 +531,22 @@ newEntity{ base = "BASE_SHIELD", resists = { [DamageType.BLIGHT] = 25, [DamageType.DARKNESS] = 25, }, inc_stats = { [Stats.STAT_WIL] = 5, }, }, - on_block = {desc = "Pull up to 1 attacker per turn, up to 15 spaces away into melee range, pinning and asphixiating them", fct = function(self, who, src, type, dam, eff) + on_block = {desc = "Up to once per turn, pull an attacker up to 15 spaces away into melee range, pinning and asphyxiating it", fct = function(self, who, src, type, dam, eff) if not src then return end if who.turn_procs.black_mesh then return end - src:pull(who.x, who.y, 15) - game.logSeen(src, "Black tendrils shoot out of the mesh and pull %s to you!", src.name:capitalize()) + who:logCombat(src, "#ORCHID#Black tendrils from #Source# grab #Target#!") + local kb = src:canBe("knockback") + if kb then + who:logCombat(src, "#ORCHID##Source#'s tendrils pull #Target# in!") + src:pull(who.x, who.y, 15) + else + game.logSeen(src, "#ORCHID#%s resists the tendrils' pull!", src.name:capitalize()) + end if core.fov.distance(who.x, who.y, src.x, src.y) <= 1 and src:canBe('pin') then src:setEffect(src.EFF_CONSTRICTED, 6, {src=who}) end - who.turn_procs.black_mesh = true - end,} } @@ -4536,7 +4540,7 @@ newEntity{ base = "BASE_SHIELD", --Thanks SageAcrin! learn_talent = { [Talents.T_BLOCK] = 2, }, max_air = 20, }, - on_block = {desc = "30% chance that a blast of freezing water will spray at the target.", fct = function(self, who, target, type, dam, eff) + on_block = {desc = "30% chance to spray freezing water (radius 4 cone) at the target.", fct = function(self, who, target, type, dam, eff) if rng.percent(30) then if not target or target:attr("dead") or not target.x or not target.y then return end @@ -4544,7 +4548,7 @@ newEntity{ base = "BASE_SHIELD", --Thanks SageAcrin! who:project(burst, target.x, target.y, engine.DamageType.ICE, 30) game.level.map:particleEmitter(who.x, who.y, burst.radius, "breath_cold", {radius=burst.radius, tx=target.x-who.x, ty=target.y-who.y}) - who:logCombat(target, "A wave of icy water bursts out from #Source#'s shield towards #Target#!") + who:logCombat(target, "A wave of icy water sprays out from #Source# towards #Target#!") end end,}, } diff --git a/game/modules/tome/data/talents/cunning/artifice.lua b/game/modules/tome/data/talents/cunning/artifice.lua index 5cad988f587c46c1379b5188452f242bc4afb83f..180151675bfbd5a55f889b97da05cae2e0f472df 100644 --- a/game/modules/tome/data/talents/cunning/artifice.lua +++ b/game/modules/tome/data/talents/cunning/artifice.lua @@ -22,75 +22,111 @@ local Object = require "engine.Object" local Map = require "engine.Map" local Chat = require "engine.Chat" +-- equipable artifice tool talents and associated mastery talents +-- to add a new tool, define a tool talent and a mastery talent and update this table +artifice_tool_tids = {T_HIDDEN_BLADES="T_ASSASSINATE", T_SMOKESCREEN="T_SMOKESCREEN_MASTERY", T_ROGUE_S_BREW="T_ROGUE_S_BREW_MASTERY", T_DART_LAUNCHER="T_DART_LAUNCHER_MASTERY"} + +--- initialize artifice tools, update mastery level and unlearn any unselected tools talents +function artifice_tools_setup(self, t) + self.artifice_tools = self.artifice_tools or {} + self:setTalentTypeMastery("cunning/tools", self:getTalentMastery(t)) + for tid, m_tid in pairs(artifice_tool_tids) do + if self:knowTalent(tid) then + local slot + for slot_id, tool_id in pairs(self.artifice_tools) do + if tool_id == tid then slot = slot_id break end + end + if not slot then self:unlearnTalentFull(tid) end + end + if self.artifice_tools_mastery == tid then + local m_level = self:getTalentLevelRaw(self.T_MASTER_ARTIFICER) + if self:getTalentLevelRaw(m_tid) ~= m_level then + self:unlearnTalentFull(m_tid) + self:learnTalent(m_tid, true, m_level, {no_unlearn=true}) + end + elseif self:knowTalent(m_tid) then + self:unlearnTalentFull(m_tid) + end + end + return true +end + +--- generate a textual list of available artifice tools +function artifice_tools_get_descs(self, t) + if not self.artifice_tools then artifice_tools_setup(self, t) end + local tool_descs = {} + for tool_id, mt in pairs(artifice_tool_tids) do + local tool, desc = self:getTalentFromId(tool_id) + local prepped = self.artifice_tools[t.id] == tool_id + if prepped then + desc = ("#YELLOW#%s (prepared, level %s)#LAST#:\n"):format(tool.name, self:getTalentLevelRaw(tool)) + else + desc = tool.name..":\n" + end + if tool.short_info then + desc = desc..tool.short_info(self, tool, t).."\n" + else + desc = desc.."#GREY#(see talent description)#LAST#\n" + end + tool_descs[#tool_descs+1] = desc + end + return table.concatNice(tool_descs, "\n\t") +end + +--- NPC's automatically pick a tool for each tool slot if needed +-- used as the talent on_pre_use_ai function +-- this causes newly spawned NPC's to prepare their tools the first time they check for usable talents +function artifice_tools_npc_select(self, t, silent, fake) + if not self.artifice_tools[t.id] then -- slot is empty: pick a tool + local tool_ids = table.keys(artifice_tool_tids) + local tid = rng.tableRemove(tool_ids) + while tid do + if not self:knowTalent(tid) then -- select the tool + self:learnTalent(tid, true, self:getTalentLevelRaw(t), {no_unlearn=true}) + self.artifice_tools[t.id] = tid + if game.party:hasMember(self) then -- cooldowns for party members + self:startTalentCooldown(t); self:startTalentCooldown(tid) + self:useEnergy() + end + game.logSeen(self, "#GREY#You notice %s has prepared: %s.", self.name:capitalize(), self:getTalentFromId(tid).name) + break + end + tid = rng.tableRemove(tool_ids) + end + end + return false -- npc's don't need to actually use the tool slot talents +end + newTalent{ name = "Rogue's Tools", type = {"cunning/artifice", 1}, points = 5, require = cuns_req_high1, cooldown = 10, - no_npc_use = true, + stamina = 0, -- forces learning stamina pool (npcs) no_unlearn_last = true, + on_pre_use = artifice_tools_setup, on_learn = function(self, t) self:attr("show_gloves_combat", 1) end, on_unlearn = function(self, t) self:attr("show_gloves_combat", -1) end, - getHBDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end, - getRBDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 4, 9)) end, - getRBResist = function(self, t) return self:combatTalentLimit(t, 1, 0.17, 0.5) end, - getRBRawHeal = function (self, t) return self:getTalentLevel(t) * 40 end, - getRBMaxHeal = function (self, t) return self:combatTalentLimit(t, 0.4, 0.10, 0.25) end, - getRBCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3)) end, - getSSDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end, - getSSSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5 - getDLDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end, - getDLSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end, + tactical = {BUFF = 2}, + on_pre_use_ai = artifice_tools_npc_select, -- NPC's automatically pick a tool action = function(self, t) - if self.artifice_hidden_blades==1 then - self:unlearnTalent(self.T_HIDDEN_BLADES) - self.artifice_hidden_blades = null - if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end - end - if self.artifice_smokescreen==1 then - self:unlearnTalent(self.T_SMOKESCREEN) - self.artifice_smokescreen = null - if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end - end - if self.artifice_rogue_s_brew==1 then - self:unlearnTalent(self.T_ROGUE_S_BREW) - self.artifice_rogue_s_brew = null - if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end - end - if self.artifice_dart_launcher==1 then - self:unlearnTalent(self.T_DART_LAUNCHER) - self.artifice_dart_launcher = null - if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end - end - - local chat = Chat.new("artifice", self, self, {player=self, slot=1}) + local chat = Chat.new("artifice", self, self, {player=self, slot=1, chat_tid=t.id, tool_ids=artifice_tool_tids}) self:talentDialog(chat:invoke()) - return true + artifice_tools_setup(self, t) + return self.turn_procs._did_artifice -- only use energy/cooldown if a tool was prepared end, info = function(self, t) - local tool = "" - if self:knowTalent(self.T_HIDDEN_BLADES) and self.artifice_hidden_blades==1 then - tool = ([[#YELLOW#Current Tool: Hidden Blades]]):format() - elseif self:knowTalent(self.T_SMOKESCREEN) and self.artifice_smokescreen==1 then - tool = ([[#YELLOW#Current Tool: Smokescreen]]):format() - elseif self:knowTalent(self.T_ROGUE_S_BREW) and self.artifice_rogue_s_brew==1 then - tool = ([[#YELLOW#Current Tool: Rogue's Brew]]):format() - elseif self:knowTalent(self.T_DART_LAUNCHER) and self.artifice_dart_launcher==1 then - tool = ([[#YELLOW#Current Tool: Dart Launcher]]):format() - end - return ([[You learn to create and equip a number of useful tools: -Hidden Blades. Melee criticals inflict %d%% bonus unarmed damage. 4 turn cooldown. -Smokescreen. Throw a vial of smoke that blocks vision in radius 2 for %d turns, and reduces the vision of enemies within by %d. 15 turn cooldown. -Rogue’s Brew. Drink a potion that restores %d life (+%d%% of maximum), %d stamina (+%d%% of maximum) and cures %d negative physical effects. 20 turn cooldown. -Dart Launcher. Fires a dart that deals %0.2f physical damage and puts the target to sleep for 4 turns. 10 turn cooldown. -You can equip a single tool at first. -%s]]): -format(t.getHBDamage(self,t)*100, t.getSSDuration(self,t), t.getSSSightLoss(self,t), t.getRBRawHeal(self,t), t.getRBMaxHeal(self,t)*100, t.getRBRawHeal(self,t)/4, t.getRBMaxHeal(self,t)*40, t.getRBCure(self,t), damDesc(self, DamageType.PHYSICAL, t.getDLDamage(self,t)), tool) + local descs = artifice_tools_get_descs(self, t) + return ([[With some advanced preparation, you learn to create and equip one of a number of useful tools (at #YELLOW#level %d#WHITE#): + +%s +Preparing a tool sets its talent level and puts it on cooldown. +]]):format(self:getTalentLevelRaw(t), descs) end, } @@ -100,128 +136,51 @@ newTalent{ points = 5, require = cuns_req_high2, cooldown = 10, - no_npc_use = true, + stamina = 0, -- forces learning stamina pool (npcs) no_unlearn_last = true, - getHBDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end, - getRBDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 4, 9)) end, - getRBResist = function(self, t) return self:combatTalentLimit(t, 1, 0.17, 0.5) end, - getRBRawHeal = function (self, t) return self:getTalentLevel(t) * 40 end, - getRBMaxHeal = function (self, t) return self:combatTalentLimit(t, 0.4, 0.10, 0.25) end, - getRBCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3)) end, - getSSDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end, - getSSSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5 - getDLDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end, - getDLSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end, + on_pre_use = artifice_tools_setup, + tactical = {BUFF = 2}, + on_pre_use_ai = artifice_tools_npc_select, -- NPC's automatically pick a tool action = function(self, t) - if self.artifice_hidden_blades==2 then - self:unlearnTalent(self.T_HIDDEN_BLADES) - self.artifice_hidden_blades = null - if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end - end - if self.artifice_smokescreen==2 then - self:unlearnTalent(self.T_SMOKESCREEN) - self.artifice_smokescreen = null - if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end - end - if self.artifice_rogue_s_brew==2 then - self:unlearnTalent(self.T_ROGUE_S_BREW) - self.artifice_rogue_s_brew = null - if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end - end - if self.artifice_dart_launcher==2 then - self:unlearnTalent(self.T_DART_LAUNCHER) - self.artifice_dart_launcher = null - if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end - end - - local chat = Chat.new("artifice", self, self, {player=self, slot=2}) + local chat = Chat.new("artifice", self, self, {player=self, slot=2, chat_tid=t.id, tool_ids=artifice_tool_tids}) self:talentDialog(chat:invoke()) - return true + return self.turn_procs._did_artifice -- only use energy/cooldown if a tool was prepared end, info = function(self, t) - local tool = "" - if self:knowTalent(self.T_HIDDEN_BLADES) and self.artifice_hidden_blades==2 then - tool = ([[#YELLOW#Current Tool: Hidden Blades]]):format() - elseif self:knowTalent(self.T_SMOKESCREEN) and self.artifice_smokescreen==2 then - tool = ([[#YELLOW#Current Tool: Smokescreen]]):format() - elseif self:knowTalent(self.T_ROGUE_S_BREW) and self.artifice_rogue_s_brew==2 then - tool = ([[#YELLOW#Current Tool: Rogue's Brew]]):format() - elseif self:knowTalent(self.T_DART_LAUNCHER) and self.artifice_dart_launcher==2 then - tool = ([[#YELLOW#Current Tool: Dart Launcher]]):format() - end - return ([[You learn to equip a second tool: -Hidden Blades. Melee criticals inflict %d%% bonus unarmed damage. 4 turn cooldown. -Smokescreen. Throw a vial of smoke that blocks vision in radius 2 for %d turns, and reduces the vision of enemies within by %d. 15 turn cooldown. -Rogue’s Brew. Drink a potion that restores %d life (+%d%% of maximum), %d stamina (+%d%% of maximum) and cures %d negative physical effects. 20 turn cooldown. -Dart Launcher. Fires a dart that deals %0.2f physical damage and puts the target to sleep for 4 turns. 10 turn cooldown. -%s]]): -format(t.getHBDamage(self,t)*100, t.getSSDuration(self,t), t.getSSSightLoss(self,t), t.getRBRawHeal(self,t), t.getRBMaxHeal(self,t)*100, t.getRBRawHeal(self,t)/4, t.getRBMaxHeal(self,t)*40, t.getRBCure(self,t), damDesc(self, DamageType.PHYSICAL, t.getDLDamage(self,t)), tool) + local descs = artifice_tools_get_descs(self, t) + return ([[With some advanced preparation, you learn to create and equip a second tool (at #YELLOW#level %d#WHITE#): + +%s +Preparing a tool sets its talent level and puts it on cooldown. +Only one tool of each type can be equipped at a time. +]]):format(self:getTalentLevelRaw(t), descs) end, } - newTalent{ name = "Intricate Tools", type = {"cunning/artifice", 3}, require = cuns_req_high3, points = 5, cooldown = 10, - no_npc_use = true, + stamina = 0, -- forces learning stamina pool (npcs) no_unlearn_last = true, - getHBDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end, - getRBDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 4, 9)) end, - getRBResist = function(self, t) return self:combatTalentLimit(t, 1, 0.17, 0.5) end, - getRBRawHeal = function (self, t) return self:getTalentLevel(t) * 40 end, - getRBMaxHeal = function (self, t) return self:combatTalentLimit(t, 0.4, 0.10, 0.25) end, - getRBCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3)) end, - getSSDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end, - getSSSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5 - getDLDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end, - getDLSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end, + on_pre_use = artifice_tools_setup, + tactical = {BUFF = 2}, + on_pre_use_ai = artifice_tools_npc_select, -- NPC's automatically pick a tool action = function(self, t) - if self.artifice_hidden_blades==3 then - self:unlearnTalent(self.T_HIDDEN_BLADES) - self.artifice_hidden_blades = null - if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end - end - if self.artifice_smokescreen==3 then - self:unlearnTalent(self.T_SMOKESCREEN) - self.artifice_smokescreen = null - if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end - end - if self.artifice_rogue_s_brew==3 then - self:unlearnTalent(self.T_ROGUE_S_BREW) - self.artifice_rogue_s_brew = null - if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end - end - if self.artifice_dart_launcher==3 then - self:unlearnTalent(self.T_DART_LAUNCHER) - self.artifice_dart_launcher = null - if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end - end - - local chat = Chat.new("artifice", self, self, {player=self, slot=3}) + local chat = Chat.new("artifice", self, self, {player=self, slot=3, chat_tid=t.id, tool_ids=artifice_tool_tids}) self:talentDialog(chat:invoke()) - return true + return self.turn_procs._did_artifice -- only use energy/cooldown if a tool was prepared end, info = function(self, t) - local tool = "" - if self:knowTalent(self.T_HIDDEN_BLADES) and self.artifice_hidden_blades==3 then - tool = ([[#YELLOW#Current Tool: Hidden Blades]]):format() - elseif self:knowTalent(self.T_SMOKESCREEN) and self.artifice_smokescreen==3 then - tool = ([[#YELLOW#Current Tool: Smokescreen]]):format() - elseif self:knowTalent(self.T_ROGUE_S_BREW) and self.artifice_rogue_s_brew==3 then - tool = ([[#YELLOW#Current Tool: Rogue's Brew]]):format() - elseif self:knowTalent(self.T_DART_LAUNCHER) and self.artifice_dart_launcher==3 then - tool = ([[#YELLOW#Current Tool: Dart Launcher]]):format() - end - return ([[You learn to equip a third tool: -Hidden Blades. Melee criticals inflict %d%% bonus unarmed damage. 4 turn cooldown. -Smokescreen. Throw a vial of smoke that blocks vision in radius 2 for %d turns, and reduces the vision of enemies within by %d. 15 turn cooldown. -Rogue’s Brew. Drink a potion that restores %d life (+%d%% of maximum), %d stamina (+%d%% of maximum) and cures %d negative physical effects. 20 turn cooldown. -Dart Launcher. Fires a dart that deals %0.2f physical damage and puts the target to sleep for 4 turns. 10 turn cooldown. -%s]]): -format(t.getHBDamage(self,t)*100, t.getSSDuration(self,t), t.getSSSightLoss(self,t), t.getRBRawHeal(self,t), t.getRBMaxHeal(self,t)*100, t.getRBRawHeal(self,t)/4, t.getRBMaxHeal(self,t)*40, t.getRBCure(self,t), damDesc(self, DamageType.PHYSICAL, t.getDLDamage(self,t)), tool) + local descs = artifice_tools_get_descs(self, t) + return ([[With some advanced preparation, you learn to create and equip a third tool (at #YELLOW#level %d#WHITE#): + +%s +Preparing a tool sets its talent level and puts it on cooldown. +Only one tool of each type can be equipped at a time. +]]):format(self:getTalentLevelRaw(t), descs) end, } @@ -231,57 +190,83 @@ newTalent{ require = cuns_req_high4, points = 5, cooldown = 10, - no_npc_use = true, + stamina = 0, -- forces learning stamina pool (npcs) + no_energy = true, no_unlearn_last = true, - getAssassinateDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.8, 3.0) end, - getBleed = function(self, t) return self:combatTalentScale(t, 0.2, 0.8) end, - getSSDamage = function (self, t) return 30 + self:combatTalentStatDamage(t, "cun", 10, 150) end, - getRBDieAt = function(self, t) return self:combatTalentScale(t, 100, 600) end, - getDLSlow = function(self, t) return self:combatTalentLimit(t, 50, 15, 40)/100 end, + on_pre_use = artifice_tools_setup, + tactical = {BUFF = 2}, + on_pre_use_ai = function(self, t, silent, fake) -- npc's automatically master a tool they have prepared + if self.artifice_tools and not self.artifice_tools_mastery then + game:onTickEnd(function() + local tools = table.values(self.artifice_tools) + while #tools > 0 do + local tool_id = rng.tableRemove(tools) + local m_tid = artifice_tool_tids[tool_id] + if m_tid then -- note: talent level affects AI use + local tl = self:getTalentLevelRaw(m_tid) + if self:learnTalent(m_tid, true, self:getTalentLevelRaw(t) - tl) then + self.artifice_tools_mastery = tool_id + if game.party:hasMember(self) then -- cooldowns for party members + self:startTalentCooldown(t); self:startTalentCooldown(tool_id); self:startTalentCooldown(m_tid) + end + break + end + end + end + end) + end + return false + end, action = function(self, t) - if self:knowTalent(self.T_ASSASSINATE) then self:unlearnTalent(self.T_ASSASSINATE) end - if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then self:unlearnTalent(self.T_SMOKESCREEN_MASTERY) end - if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:unlearnTalent(self.T_ROGUE_S_BREW_MASTERY) end - if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then self:unlearnTalent(self.T_DART_LAUNCHER_MASTERY) end - - local chat = Chat.new("artifice-mastery", self, self, {player=self}) + local chat = Chat.new("artifice-mastery", self, self, {player=self, chat_tid=t.id, tool_ids=artifice_tool_tids}) self:talentDialog(chat:invoke()) - return true + return false -- chat handles cooldowns end, info = function(self, t) - local tool = "" - if self:knowTalent(self.T_ASSASSINATE) then - tool = ([[#YELLOW#Current Mastery: Hidden Blades]]):format() - elseif self:knowTalent(self.T_SMOKESCREEN_MASTERY) then - tool = ([[#YELLOW#Current Mastery: Smokescreen]]):format() - elseif self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then - tool = ([[#YELLOW#Current Mastery: Rogue's Brew]]):format() - elseif self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then - tool = ([[#YELLOW#Current Mastery: Dart Launcher]]):format() + local tool = "none" + if self.artifice_tools_mastery then + tool = self:getTalentFromId(self.artifice_tools_mastery).name + end + --- generate a textual list of available artifice tools enhancements + if not self.artifice_tools then artifice_tools_setup(self, t) end + local mastery_descs = {} + for tool_id, m_tid in pairs(artifice_tool_tids) do + local tool, mt = self:getTalentFromId(tool_id), self:getTalentFromId(m_tid) + if mt then + local desc + local prepped = self.artifice_tools_mastery == tool_id + if prepped then + desc = ("#YELLOW#%s (%s)#LAST#\n"):format(tool.name, mt.name) + else + desc = ("%s (%s)\n"):format(tool.name, mt.name) + end + if mt.short_info then + desc = desc..mt.short_info(self, mt).."\n" + else + desc = desc.."#GREY#(see talent description)#LAST#\n" + end + mastery_descs[#mastery_descs+1] = desc + end end - return ([[You reach the height of your craft, allowing you to focus on a single tool to greatly improve its capabilities: -Hidden Blades. Grants use of the Assassinate ability, striking twice with your hidden blades for %d%% unarmed damage as a guaranteed critical strike which ignores armor and resistances. Your Hidden Blades also inflict an additional %d%% damage as bleed. -Smokescreen: Infuses your Smokescreen with chokedust, causing %0.2f nature damage each turn to enemies inside as well as silencing them. -Rogue’s Brew. The brew strengthens you for 8 turns, preventing you from dying until you reach -%d life. -Dart Launcher. The sleeping poison becomes potent enough to ignore immunity, and on waking the target will be slowed by %d%% for 4 turns. -%s]]): -format(t.getAssassinateDamage(self,t)*100, t.getBleed(self,t)*100, damDesc(self, DamageType.NATURE, t.getSSDamage(self,t)), t.getRBDieAt(self,t), t.getDLSlow(self,t)*100, tool) + mastery_descs = table.concatNice(mastery_descs, "\n\t") + return ([[You become a master of your craft, allowing you to focus on a single tool {#YELLOW#currently %s#LAST#) to greatly improve its capabilities: + +%s +The effects depend on this talent's level. +Mastering a new tool places it (and its special effects, as appropriate) on cooldown.]]):format(tool, mastery_descs) end, } +--====================-- +-- Rogue's tools and enhancements +--====================-- newTalent{ name = "Hidden Blades", type = {"cunning/tools", 1}, mode = "passive", points = 1, cooldown = 4, - getDamage = function(self, t) - if self.artifice_hidden_blades == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getHBDamage") - elseif self.artifice_hidden_blades == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getHBDamage") - elseif self.artifice_hidden_blades == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getHBDamage") - else return 0 - end - end, + getDamage = function (self, t) return self:combatTalentWeaponDamage(t, 1.0, 1.8) end, callbackOnCrit = function(self, t, kind, dam, chance, target) if not target then return end if target.turn_procs.hb then return end @@ -289,6 +274,7 @@ newTalent{ if not self:isTalentCoolingDown(t) then target.turn_procs.hb = true local oldlife = target.life + self:logCombat(target, "#Source# strikes #target# with hidden blades!") self:attackTarget(target, nil, t.getDamage(self,t), true, true) if self:knowTalent(self.T_ASSASSINATE) then @@ -302,47 +288,112 @@ newTalent{ self:startTalentCooldown(t) end end, + short_info = function(self, t, slot_talent) + return ([[Melee criticals trigger an extra unarmed attack, inflicting %d%% damage. 4 turn cooldown.]]):format(t.getDamage(self, slot_talent)*100) + end, info = function(self, t) local dam = t.getDamage(self, t) - return ([[You mount spring loaded blades on your wrists. On scoring a critical strike against an adjacent target, you follow up with your blades for %d%% unarmed damage. -This talent has a cooldown.]]): - format(dam*100) + local slot = "not prepared" + for slot_id, tool_id in pairs(self.artifice_tools) do + if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end + end + return ([[You conceal spring loaded blades within your equipment. On scoring a critical strike against an adjacent target, you follow up with your blades for %d%% damage (as an unarmed attack). +This talent has a cooldown. +#YELLOW#Prepared with: %s#LAST#]]):format(dam*100, slot) end, } newTalent{ - name = "Rogue's Brew", + name = "Assassinate", type = {"cunning/tools", 1}, points = 1, - cooldown = 20, - tactical = { BUFF = 2 }, + cooldown = 8, + stamina = 10, + message = false, + tactical = { ATTACK = 3 }, requires_target = true, - getRawHeal = function(self, t) - if self.artifice_rogue_s_brew == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getRBRawHeal") - elseif self.artifice_rogue_s_brew == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getRBRawHeal") - elseif self.artifice_rogue_s_brew == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getRBRawHeal") - else return 0 + is_melee = true, + target = function(self, t) return {type="hit", range=self:getTalentRange(t)} end, + range = 1, + on_pre_use = function(self, t, silent, fake) + if not self:knowTalent(self.T_HIDDEN_BLADES) then + if not silent then game.logPlayer(self, "You must have Hidden Blades prepared to use this talent.") end + return end + return true end, - getMaxHeal = function(self, t) - if self.artifice_rogue_s_brew == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getRBMaxHeal") - elseif self.artifice_rogue_s_brew == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getRBMaxHeal") - elseif self.artifice_rogue_s_brew == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getRBMaxHeal") - else return 0 + getDamage = function (self, t) return self:combatTalentWeaponDamage(self:getTalentFromId(self.T_MASTER_ARTIFICER), 1.8, 3.0) end, + getBleed = function(self, t) return self:combatTalentScale(self:getTalentFromId(self.T_MASTER_ARTIFICER), 0.3, 1) end, + action = function(self, t) + local tg = self:getTalentTarget(t) + local x, y, target = self:getTarget(tg) + if not target or not self:canSee(target) or not self:canProject(tg, x, y) then return nil end + + target.turn_procs.hb = true -- prevent a crit against this target from triggering an additional hidden blades attack + self.turn_procs.auto_melee_hit = true + -- store old values to restore later + local apr, rpen, evasion = self.combat_apr, self.resists_pen.PHYSICAL, target.evasion + self:attr("combat_apr", 10000) + self.resists_pen.PHYSICAL = 100 + target.evasion = 0 + local bleed = t.getBleed(self, t) + local oldlife = target.life + + self:logCombat(target, "#Source# strikes at a vital spot on #target#!") + local do_attack = function() self:attackTarget(target, nil, t.getDamage(self, t), true, true) end + local ok, err = pcall(do_attack) + if ok then ok, err = pcall(do_attack) end + self.combat_apr, self.resists_pen.PHYSICAL, target.evasion = apr, rpen, evasion + if not ok then error(err) end + self.turn_procs.auto_melee_hit = nil + + local life_diff = oldlife - target.life + if life_diff > 0 and target:canBe('cut') and bleed then + target:setEffect(target.EFF_CUT, 5, {power=life_diff * bleed / 5, src=self}) end + + return true + end, + short_info = function(self, t) + return ([[You prime your Hidden Blades to cause bleeding and facilitate the Assassinate ability, which allows you to strike twice for %d%% unarmed damage, hitting automatically while ignoring armor and resistance.]]):format(t.getDamage(self, t)*100) + end, + info = function(self, t) + local damage = t.getDamage(self, t) * 100 + local bleed = t.getBleed(self,t) * 100 + return ([[You strike your target with your Hidden Blades twice in a vital spot for %d%% unarmed (physical) damage. You must be able to see your target to use this attack, but it always hits and ignores all armor and physical resistance. +In addition, your hidden blades now inflict a further %d%% of all damage dealt as bleeding over 5 turns.]]) + :format(damage, bleed) end, - getCure = function(self,t) - if self.artifice_rogue_s_brew == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getRBCure") - elseif self.artifice_rogue_s_brew == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getRBCure") - elseif self.artifice_rogue_s_brew == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getRBCure") - else return 0 +} + +newTalent{ + name = "Rogue's Brew", + type = {"cunning/tools", 1}, + points = 1, + cooldown = 20, + tactical = { HEAL = 1.5, STAMINA = 1.5, + CURE = function(self, t, target) + local num, max = 0, t.getCure(self, t) + for eff_id, p in pairs(self.tmp) do + local e = self.tempeffect_def[eff_id] + if e.type == "physical" and e.status == "detrimental" then + num = num + 1 + if num >= max then break end + end + end + return (2*num)^.5 end + }, + getHeal = function(self, t) + return self:combatStatScale("cun", 10, 200, 0.7) + self:combatTalentScale(t, 20, 200, 0.7) end, - getDieAt = function(self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getRBDieAt") end, + getStam = function(self, t) + return self:combatStatScale("cun", 5, 50, 0.75) + self:combatTalentScale(t, 5, 50, 0.75) + end, + getCure = function(self, t) return math.floor(self:combatTalentScale(t, 1, 3, "log")) end, action = function(self, t) - - local life = t.getRawHeal(self,t) + (t.getMaxHeal(self,t) * self.max_life) - local sta = t.getRawHeal(self,t)/4 + (t.getMaxHeal(self,t) * self.max_stamina * 0.4) + local life = t.getHeal(self, t) + local sta = t.getStam(self, t) self:incStamina(sta) self:attr("allow_on_heal", 1) self:heal(life, self) @@ -370,20 +421,41 @@ newTalent{ game.logSeen(self, "%s is cured!", self.name:capitalize()) end - if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:setEffect(self.EFF_ROGUE_S_BREW, 8, {power = t.getDieAt(self,t)}) end + if self:knowTalent(self.T_ROGUE_S_BREW_MASTERY) then self:setEffect(self.EFF_ROGUE_S_BREW, 8, {power = self:callTalent(self.T_ROGUE_S_BREW_MASTERY, "getDieAt")}) end return true end, + short_info = function(self, t, slot_talent) + return ([[Prepare a potion that restores %d life, %d stamina, and cures %d negative physical effects. 20 turn cooldown.]]):format(t.getHeal(self, slot_talent), t.getStam(self, slot_talent), t.getCure(self, slot_talent)) + end, info = function(self, t) - local heal = t.getRawHeal(self,t) + (t.getMaxHeal(self,t) * self.max_life) - local sta = t.getRawHeal(self,t)/4 + (t.getMaxHeal(self,t) * self.max_stamina * 0.4) + local heal = t.getHeal(self, t) + local sta = t.getStam(self, t) local cure = t.getCure(self,t) - return ([[Imbibe a potent mixture of energizing and restorative substances, restoring %d life, %d stamina and curing %d negative physical effects.]]): - format(heal, sta, cure) + local slot = "not prepared" + for slot_id, tool_id in pairs(self.artifice_tools) do + if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end + end + return ([[Imbibe a potent mixture of energizing and restorative substances, restoring %d life, %d stamina and curing %d detrimental physical effects. The restorative effects improve with your Cunning. + #YELLOW#Prepared with: %s#LAST#]]):format(heal, sta, cure, slot) end, } +newTalent{ + name = "Rogue's Brew Mastery", + type = {"cunning/tools", 1}, + mode = "passive", + points = 1, + getDieAt = function(self, t) return self:combatTalentScale(self:getTalentFromId(self.T_MASTER_ARTIFICER), 100, 600) end, + short_info = function(self, t) + return ([[Your Rogue's Brew fortifies you for 8 turns, preventing you from dying until you reach -%d life.]]):format(t.getDieAt(self, t)) + end, + info = function(self, t) + return ([[Adjust your Rogue's Brew formulation so that it fortifies you for 8 turns, preventing you from dying until you reach -%d life.]]):format(t.getDieAt(self,t)) + end, +} + newTalent{ name = "Smokescreen", type = {"cunning/tools", 1}, @@ -392,32 +464,22 @@ newTalent{ stamina = 10, range = 6, direct_hit = true, - tactical = { DISABLE = 2 }, + tactical = { ESCAPE = 2, DISABLE = {blind = 2} }, requires_target = true, + no_break_stealth = true, radius = 2, - getSightLoss = function(self, t) - if self.artifice_smokescreen == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getSSSightLoss") - elseif self.artifice_smokescreen == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getSSSightLoss") - elseif self.artifice_smokescreen == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getSSSightLoss") - else return 0 - end - end, getDamage = function(self,t) if self:knowTalent(self.T_SMOKESCREEN_MASTERY) then - return self:callTalent(self.T_SMOKESCREEN_MASTERY, "getSSDamage") + return self:callTalent(self.T_SMOKESCREEN_MASTERY, "getDamage") else return 0 end end, - getDuration = function(self, t) - if self.artifice_smokescreen == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getSSDuration") - elseif self.artifice_smokescreen == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getSSDuration") - elseif self.artifice_smokescreen == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getSSDuration") - else return 0 - end - end, + getDuration = function(self, t) return math.ceil(self:combatTalentScale(t, 3, 5)) end, + getSightLoss = function(self, t) return math.floor(self:combatTalentScale(t,1, 6, "log", 0, 4)) end, -- 1@1 6@5 + target = function(self, t) return {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), talent=t} end, action = function(self, t) - local tg = {type="ball", range=self:getTalentRange(t), radius=self:getTalentRadius(t), talent=t} + local tg = self:getTalentTarget(t) local x, y = self:getTarget(tg) if not x or not y then return nil end @@ -460,7 +522,7 @@ newTalent{ e.particles = Particles.new("creeping_dark", 1, { }) e.particles.x = px e.particles.y = py - game.level.map:addParticleEmitter(e.particles) + game.level.map:addParticleEmitter(e.particles) end, nil, {type="dark"}) @@ -468,80 +530,19 @@ newTalent{ game.level.map:redisplay() return true end, - info = function(self, t) - return ([[Throw a vial of sticky smoke that explodes in radius %d, blocking line of sight for 5 turns. Enemies within will have their vision range reduced by %d. - Creatures affected by smokescreen can never prevent you from stealthing, even if their proximity would normally forbid it. - Use of this will not break stealth.]]): - format(self:getTalentRadius(t), t.getSightLoss(self,t)) - end, -} - -newTalent{ - name = "Assassinate", - type = {"cunning/tools", 1}, - points = 1, - cooldown = 8, - message = "@Source@ lashes out with their hidden blades!", - tactical = { ATTACK = { weapon = 2 } }, - requires_target = true, - is_melee = true, - target = function(self, t) return {type="hit", range=self:getTalentRange(t)} end, - range = 1, - getDamage = function(self, t) return self:callTalent(self.T_MASTER_ARTIFICER, "getAssassinateDamage") end, - getBleed = function(self, t) return self:combatTalentScale(t, 0.3, 1) end, - action = function(self, t) - local tg = self:getTalentTarget(t) - local x, y, target = self:getTarget(tg) - if not target or not self:canProject(tg, x, y) then return nil end - - target.turn_procs.hb = true -- we're already using our hidden blades for this attack - self.turn_procs.auto_melee_hit = true - - self:attr("combat_apr", 1000) - local penstore = self.resists_pen - local storeeva = target.evasion - target.evasion=0 - self.resists_pen = nil - self.resists_pen = {all = 100} - - local scale = nil - scale = t.getBleed(self, t) - local oldlife = target.life - - self:attackTarget(target, nil, t.getDamage(self, t), true, true) - self:attackTarget(target, nil, t.getDamage(self, t), true, true) - - local life_diff = oldlife - target.life - if life_diff > 0 and target:canBe('cut') and scale then - target:setEffect(target.EFF_CUT, 5, {power=life_diff * scale / 5, src=self}) - end - - self:attr("combat_apr", -1000) - self.turn_procs.auto_melee_hit = nil - target.evasion = storeeva - self.resists_pen = nil - self.resists_pen = penstore - - return true - end, - info = function(self, t) - local damage = t.getDamage(self, t) * 100 - local bleed = t.getBleed(self,t) * 100 - return ([[Impale the target on your hidden blades, striking twice for %d%% unarmed damage. This attack always hits and ignores all armor and resistances. -In addition, your hidden blades now inflict a further %d%% of all damage dealt as bleeding over 5 turns.]]) - :format(damage, bleed) + short_info = function(self, t, slot_talent) + return ([[Throw a smokebomb creating a radius 2 cloud of smoke, lasting %d turns, that blocks sight and reduces enemies' vision by %d. 15 turn cooldown.]]):format(t.getSightLoss(self, slot_talent), t.getDuration(self, slot_talent)) end, -} - -newTalent{ - name = "Rogue's Brew Mastery", - type = {"cunning/tools", 1}, - mode = "passive", - points = 1, - getDieAt = function(self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getRBDieAt") end, info = function(self, t) - return ([[The brew strengthens you for 8 turns, preventing you from dying until you reach -%d life.]]): - format(t.getDieAt(self,t)) + local slot = "not prepared" + for slot_id, tool_id in pairs(self.artifice_tools) do + if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end + end + return ([[Throw a vial of volatile liquid that explodes in a smoke cloud of radius %d, blocking line of sight for 5 turns. Enemies within will have their vision range reduced by %d. + Creatures affected by smokescreen can never prevent you from activating stealth, even if their proximity would normally forbid it. + Use of this talent will not break stealth. + #YELLOW#Prepared with: %s#LAST#]]): + format(self:getTalentRadius(t), t.getSightLoss(self,t), slot) end, } @@ -550,12 +551,13 @@ newTalent{ type = {"cunning/tools", 1}, points = 1, mode = "passive", - getSSDamage = function (self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getSSDamage") end, - getSSEvasion = function (self,t) return self:callTalent(self.T_MASTER_ARTIFICER, "getSSEvasion") end, - no_npc_use = true, + getDamage = function (self, t) return 30 + self:combatTalentStatDamage(self:getTalentFromId(self.T_MASTER_ARTIFICER), "cun", 10, 150) end, + short_info = function(self, t) + return ([[Your Smokescreen is infused with chokedust. Enemies in the smoke take %0.2f nature damage and may be silenced.]]):format(t.getDamage(self, t)) + end, info = function(self, t) - return ([[Infuses your smoke bomb with chokedust, causing %0.2f nature damage each turn and silencing enemies inside.]]): - format(damDesc(self, DamageType.NATURE, t.getSSDamage(self,t)), t.getSSEvasion(self,t)) + return ([[You infuse your smoke bomb with chokedust. Each turn, enemies in the smoke take %0.2f nature damage and are 50%% likely to be silenced.]]): + format(damDesc(self, DamageType.NATURE, t.getDamage(self,t))) end, } @@ -563,31 +565,23 @@ newTalent{ name = "Dart Launcher", type = {"cunning/tools", 1}, points = 1, - tactical = { ATTACK = 2 }, + tactical = { ATTACK = {PHYSICAL = 1}, + DISABLE = function(self, t, target) + return target:checkClassification("unliving") and 0 or self:knowTalent(self.T_DART_LAUNCHER_MASTERY) and 2 or {sleep = 1, poison = 1} + end + }, range = 5, no_energy = true, cooldown = 10, + stamina = 5, requires_target = true, no_break_stealth = true, - getDamage = function(self, t) - if self.artifice_dart_launcher == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getDLDamage") - elseif self.artifice_dart_launcher == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getDLDamage") - elseif self.artifice_dart_launcher == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getDLDamage") - else return 0 - end - end, - getSleepPower = function(self, t) - if self.artifice_dart_launcher == 1 then return self:callTalent(self.T_ROGUE_S_TOOLS, "getDLSleepPower") - elseif self.artifice_dart_launcher == 2 then return self:callTalent(self.T_CUNNING_TOOLS, "getDLSleepPower") - elseif self.artifice_dart_launcher == 3 then return self:callTalent(self.T_INTRICATE_TOOLS, "getDLSleepPower") - else return 0 - end - end, - getSlow = function(self, t) return self:callTalent(self.T_MASTER_ARTIFICER, "getDLSlow") end, + getDamage = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 12, 150) end, + getSleepPower = function(self, t) return 15 + self:combatTalentStatDamage(t, "cun", 15, 180) end, target = function(self, t) return {type="bolt", range=self:getTalentRange(t)} end, - action = function(self, t) + 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 @@ -595,29 +589,37 @@ newTalent{ local slow = 0 - if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then slow = t.getSlow(self,t) end + if self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then slow = self:callTalent(self.T_DART_LAUNCHER_MASTERY, "getSlow") end self:project(tg, x, y, function(px, py) local target = game.level.map(px, py, engine.Map.ACTOR) if not target then return nil end self:project(tg, x, y, DamageType.PHYSICAL, t.getDamage(self,t)) - if (target:canBe("sleep") and target:canBe("poison")) or self:knowTalent(self.T_DART_LAUNCHER_MASTERY) then + if target:checkClassification("living") and (self:knowTalent(self.T_DART_LAUNCHER_MASTERY) or target:canBe("sleep") and target:canBe("poison")) then target:setEffect(target.EFF_SEDATED, 4, {src=self, power=t.getSleepPower(self,t), slow=slow, insomnia=20, no_ct_effect=true, apply_power=self:combatAttack()}) game.level.map:particleEmitter(target.x, target.y, 1, "generic_charge", {rm=180, rM=200, gm=100, gM=120, bm=30, bM=50, am=70, aM=180}) else - game.logSeen(self, "%s resists the sleep!", target.name:capitalize()) + game.logSeen(self, "%s resists the sedation!", target.name:capitalize()) end end) return true end, + short_info = function(self, t, slot_talent) + return ([[Fire a poisoned dart dealing %0.2f physical damage that puts the target to sleep for 4 turns. 10 turn cooldown.]]):format(t.getDamage(self, slot_talent)) + end, info = function(self, t) local dam = t.getDamage(self,t) local power = t.getSleepPower(self,t) - return ([[Uses a wrist mounted launcher to fire a poisoned dart dealing %0.2f physical damage and putting the target to sleep for 4 turns, rendering them unable to act. Every %d points of damage the target take reduces the duration of the sleeping poison by 1 turn. -This can be used without breaking stealth.]]): - format(damDesc(self, DamageType.PHYSICAL, dam), power) + local slot = "not prepared" + for slot_id, tool_id in pairs(self.artifice_tools) do + if tool_id == t.id then slot = self:getTalentFromId(slot_id).name break end + end + return ([[Fire a poisoned dart from a silent, concealed launcher on your person that deals %0.2f physical damage and puts the target (living only) to sleep for 4 turns, rendering them unable to act. Every %d points of damage the target takes brings it closer to waking by 1 turn. +This can be used without breaking stealth. +#YELLOW#Prepared with: %s#LAST#]]): + format(damDesc(self, DamageType.PHYSICAL, dam), power, slot) end, } @@ -626,9 +628,12 @@ newTalent{ type = {"cunning/tools", 1}, mode = "passive", points = 1, - getSlow = function(self, t) return self:callTalent(self.T_MASTER_ARTIFICER, "getDLSlow") end, + getSlow = function(self, t) return self:combatTalentLimit(self:getTalentFromId(self.T_MASTER_ARTIFICER), 50, 15, 40)/100 end, + short_info = function(self, t) + return ([[Your darts ignore poison and sleep immunity and waking targets are slowed by %d%% for 4 turns.]]):format(t.getSlow(self, t)*100) + end, info = function(self, t) return ([[The sleeping poison of your Dart Launcher becomes potent enough to ignore immunity, and upon waking the target is slowed by %d%% for 4 turns.]]): format(t.getSlow(self, t)*100) end, -} \ No newline at end of file +} diff --git a/game/modules/tome/data/talents/misc/misc.lua b/game/modules/tome/data/talents/misc/misc.lua index ac40370605954ac6ff34cdc89d4a12904f51ba1a..66a269f408274043ba6118c9e5a7d186a0d17ced 100644 --- a/game/modules/tome/data/talents/misc/misc.lua +++ b/game/modules/tome/data/talents/misc/misc.lua @@ -60,7 +60,7 @@ newTalent{ local target = game.level.map(x, y, game.level.map.ACTOR) if not target then if swap then doWardenWeaponSwap(self, t, "bow") end - return nil + return true -- Make sure this is done if an NPC attacks an emptry grid. end local did_alternate = false diff --git a/game/modules/tome/data/talents/spells/staff-combat.lua b/game/modules/tome/data/talents/spells/staff-combat.lua index 2b8ce6b1c5bbb9f457901f3bd7b05810c28ee125..c92ee4577b0a75016c866e48eac1fd51d0048de9 100644 --- a/game/modules/tome/data/talents/spells/staff-combat.lua +++ b/game/modules/tome/data/talents/spells/staff-combat.lua @@ -35,6 +35,7 @@ newTalent{ friendlyblock=false, } end, + on_pre_use = function(self, t, silent) if not self:hasStaffWeapon() then if not silent then game.logPlayer(self, "You need a staff to use this spell.") end return false end return true end, getDamageMod = function(self, t) return self:combatTalentWeaponDamage(t, 0.4, 1.1) end, action = function(self, t) local weapon = self:hasStaffWeapon() @@ -147,6 +148,7 @@ newTalent{ tactical = { ATTACK = 1, DISABLE = 2, ESCAPE = 1 }, range = 1, requires_target = true, + on_pre_use = function(self, t, silent) if not self:hasStaffWeapon() then if not silent then game.logPlayer(self, "You need a staff to use this spell.") end return false end return true end, target = function(self, t) return {type="hit", range=self:getTalentRange(t)} end,