-- ToME - Tales of Maj'Eyal -- Copyright (C) 2009, 2010, 2011, 2012, 2013 Nicolas Casalini -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see . -- -- Nicolas Casalini "DarkGod" -- darkgod@te4.org -- Modifications by C. Lowe/CaptainTrip and Mark Carpenter/Marson -- Additional Modifications by Dracos. local _M = loadPrevious(...) local Map = require "engine.Map" local function spotHostiles(self, actors_only) local seen = {} if not self.x then return seen end -- Check for visible monsters, only see LOS actors, so telepathy wont prevent resting core.fov.calc_circle(self.x, self.y, game.level.map.w, game.level.map.h, self.sight or 10, function(_, x, y) return game.level.map:opaque(x, y) end, function(_, x, y) local actor = game.level.map(x, y, game.level.map.ACTOR) if actor and self:reactionToward(actor) < 0 and self:canSee(actor) and game.level.map.seens(x, y) then seen[#seen + 1] = {x=x,y=y,actor=actor, entity=actor, name=actor.name} end end, nil) if not actors_only then -- Check for projectiles in line of sight core.fov.calc_circle(self.x, self.y, game.level.map.w, game.level.map.h, self.sight or 10, function(_, x, y) return game.level.map:opaque(x, y) end, function(_, x, y) local proj = game.level.map(x, y, game.level.map.PROJECTILE) if not proj or not game.level.map.seens(x, y) then return end -- trust ourselves but not our friends if proj.src and self == proj.src then return end local sx, sy = proj.start_x, proj.start_y local tx, ty -- Bresenham is too so check if we're anywhere near the mathematical line of flight if type(proj.project) == "table" then tx, ty = proj.project.def.x, proj.project.def.y elseif proj.homing then tx, ty = proj.homing.target.x, proj.homing.target.y end if tx and ty then local dist_to_line = math.abs((self.x - sx) * (ty - sy) - (self.y - sy) * (tx - sx)) / core.fov.distance(sx, sy, tx, ty) local our_way = ((self.x - x) * (tx - x) + (self.y - y) * (ty - y)) > 0 if our_way and dist_to_line < 1.0 then seen[#seen+1] = {x=x, y=y, projectile=proj, entity=proj, name=(proj.getName and proj:getName()) or proj.name} end end end, nil) end return seen end --- Can we continue resting ? -- We can rest if no hostiles are in sight, and if we need life/mana/stamina/psi (and their regen rates allows them to fully regen) function _M:restCheck() if game:hasDialogUp(1) then return false, "dialog is displayed" end if config.settings.marson.explore_telepathy == "Reset" then marson.seensESP = setmetatable({}, {__mode="k"}) end local spotted = spotHostiles(self) if #spotted > 0 then for _, node in ipairs(spotted) do node.entity:addParticles(engine.Particles.new("notice_enemy", 1)) end local dir = game.level.map:compassDirection(spotted[1].x - self.x, spotted[1].y - self.y) return false, ("hostile spotted to the %s (%s%s)"):format(dir or "???", spotted[1].name, game.level.map:isOnScreen(spotted[1].x, spotted[1].y) and "" or " - offscreen") end -- Resting improves regen for act, def in pairs(game.party.members) do if game.level:hasEntity(act) and not act.dead then local perc = math.min(self.resting.cnt / 10, 8) local old_shield = act.arcane_shield act.arcane_shield = nil act:heal(act.life_regen * perc) act.arcane_shield = old_shield act:incStamina(act.stamina_regen * perc) act:incMana(act.mana_regen * perc) act:incPsi(act.psi_regen * perc) end end -- Reload local ammo = self:hasAmmo() if ammo and ammo.combat.shots_left < ammo.combat.capacity then return true elseif marson.quiver_switched == true then self:quickSwitchWeapons() marson.quiver_switched = nil return true end -- Reload ammo in QS_QUIVER local function has_QS_Ammo(type) if not self:getInven("QS_QUIVER") then return nil, "no ammo" end local QS_ammo = self:getInven("QS_QUIVER")[1] if not QS_ammo then return nil, "no ammo" end if not QS_ammo.archery_ammo then return nil, "bad ammo" end if not QS_ammo.combat then return nil, "bad ammo" end if not QS_ammo.combat.capacity then return nil, "bad ammo" end if type and type ~= QS_ammo.archery_ammo then return nil, "bad type" end return QS_ammo end local QS_ammo = has_QS_Ammo() -- Check if the off-hand quiver needs to be reloaded, if so, switch to reload it. if QS_ammo and QS_ammo.combat.shots_left < QS_ammo.combat.capacity and not marson.quiver_switched then marson.quiver_switched = true self:quickSwitchWeapons() return true end -- Spacetime Tuning handles Paradox regen if self:hasEffect(self.EFF_SPACETIME_TUNING) then return true end -- Helper function to check if we can use an inscription to speed up rest. local function tryHealInscription() -- Choosing an arbitrary value here, cooldown of an average Healing infusion. if (self.life + self.life_regen * 4) < self.max_life then -- find first inscription with heal local healInscription = nil for _,inscription in ipairs(self.inscriptions) do -- if it is a heal AND could be used if self.inscriptions_data[inscription].heal and self:preUseTalent(self.talents_def["T_"..inscription], true, true) then healInscription = "T_"..inscription -- convert to actual talent to call. Wish I knew a better way to do this. break end end -- See if we can use it. if healInscription and self:preUseTalent(self.talents_def[healInscription], true, true) then self:useTalent(healInscription,self,nil,nil,nil,true,true) return true end end end -- Helper function to check if we can use an inscription to speed up rest for mana. local function tryManaInscription() -- Choosing an arbitrary value here, cooldown of an average Manasurge infusion. if (self.mana + self.mana_regen * 25) < self.max_mana then print(self.mana_regen) -- find first inscription with heal local manaInscription = nil for _,inscription in ipairs(self.inscriptions) do if self.inscriptions_data[inscription].mana and self:preUseTalent(self.talents_def["T_"..inscription], true, true) then manaInscription = "T_"..inscription -- convert to actual talent to call. Wish I knew a better way to do this. break end end -- See if we can use it. if manaInscription and self:preUseTalent(self.talents_def[manaInscription], true, true) then self:useTalent(manaInscription,self,nil,nil,nil,true,true) return true end end end -- Helper function to check if we can use a regen or heal to speed up equilibrium. It assumes -- Heal will work too, since it will trigger fungal growth, a prereq for ancestral life anyway. local function tryRegenInscriptionForEquil() -- Choosing an arbitrary value here, cooldown of an average Healing infusion. if self:knowTalent(self.T_ANCESTRAL_LIFE) and (self:getEquilibrium() + self.equilibrium_regen*4) > self:getMinEquilibrium() then -- find first inscription with heal local healInscription = nil for _,inscription in ipairs(self.inscriptions) do -- if it is a heal AND could be used if self.inscriptions_data[inscription].heal and self:preUseTalent(self.talents_def["T_"..inscription], true, true) then healInscription = "T_"..inscription -- convert to actual talent to call. Wish I knew a better way to do this. break end end -- See if we can use it. if healInscription and self:preUseTalent(self.talents_def[healInscription], true, true) then self:useTalent(healInscription,self,nil,nil,nil,true,true) return true end end end -- Check resources, make sure they CAN go up, otherwise we will never stop if not self.resting.rest_turns then if self.air_regen < 0 then return false, "losing breath!" end if self.life_regen <= 0 then return false, "losing health!" end if self:getMana() < self:getMaxMana() and self.mana_regen > 0 then if tryManaInscription() then marson.checkCooldowns = true end return true end if self:getStamina() < self:getMaxStamina() and self.stamina_regen > 0 then return true end if self:getPsi() < self:getMaxPsi() and self.psi_regen > 0 then return true end if self:getVim() < self:getMaxVim() and self.vim_regen > 0 then return true end if self:getEquilibrium() > self:getMinEquilibrium() and self.equilibrium_regen < 0 then if tryRegenInscriptionForEquil() then marson.checkCooldowns = true end return true end if self.life < self.max_life and self.life_regen> 0 then if tryHealInscription() then marson.checkCooldowns = true end return true end if self.air < self.max_air and self.air_regen > 0 and not self.is_suffocating then return true end for act, def in pairs(game.party.members) do if game.level:hasEntity(act) and not act.dead then if act.life < act.max_life and act.life_regen > 0 and not act:attr("no_life_regen") then return true end end end if ammo and ammo.combat.shots_left < ammo.combat.capacity then return true end -- Check for detrimental effects for id, _ in pairs(self.tmp) do local def = self.tempeffect_def[id] if def.type ~= "other" and def.status == "detrimental" and (def.decrease or 1) > 0 then return true end end if self:fireTalentCheck("callbackOnRest", "check") then return true end else return true end -- Enter cooldown waiting rest if we are at max already if self.resting.cnt == 0 then self.resting.wait_cooldowns = true end if self.resting.wait_cooldowns or marson.checkCooldowns then for tid, cd in pairs(self.talents_cd) do if self:isTalentActive(self.T_CONDUIT) and (tid == self.T_KINETIC_AURA or tid == self.T_CHARGED_AURA or tid == self.T_THERMAL_AURA) then -- nothing -- elseif self.talents_auto[tid] then -- if cd > 0 then return true end else if cd > 0 then return true end end end for tid, sus in pairs(self.talents) do local p = self:isTalentActive(tid) if p and p.rest_count and p.rest_count > 0 then return true end end for inven_id, inven in pairs(self.inven) do for _, o in ipairs(inven) do local cd = o:getObjectCooldown(self) if cd and cd > 0 then return true end end end end self.resting.wait_cooldowns = nil -- Enter full recharge rest if we waited for cooldowns already if self.resting.cnt == 0 then self.resting.wait_powers = true end if self.resting.wait_powers or marson.checkCooldowns then for inven_id, inven in pairs(self.inven) do for _, o in ipairs(inven) do if o.power and o.power_regen and o.power_regen > 0 and o.power < o.max_power then return true end end end end self.resting.wait_powers = nil marson.checkCooldowns = nil -- Enter recall waiting rest if we are at max already if self.resting.cnt == 0 and self:hasEffect(self.EFF_RECALL) then self.resting.wait_recall = true end if self.resting.wait_recall then if self:hasEffect(self.EFF_RECALL) then return true end end self.resting.wait_recall = nil self.resting.rested_fully = true return false, "all resources and life at maximum" end --- Called before taking a hit, overload mod.class.Actor:onTakeHit() to stop resting and running function _M:onTakeHit(value, src, death_note) self:runStop("taken damage") local ret = mod.class.Actor.onTakeHit(self, value, src, death_note) -- Ignore small damage with regards to resting as we're actively recovering anyway until its done with. if self.life < self.max_life * 0.5 or ret > self.max_life * 0.05 then self:restStop("taken damage") end -- Warn player if low health if self.life < self.max_life * 0.3 then local sx, sy = game.level.map:getTileToScreen(self.x, self.y) game.flyers:add(sx, sy, 30, (rng.range(0,2)-1) * 0.5, 2, "LOW HEALTH!", {255,0,0}, true) end -- Hit direction warning if src.x and src.y and (self.x ~= src.x or self.y ~= src.y) then local range = core.fov.distance(src.x, src.y, self.x, self.y) if range > 1 then local angle = math.atan2(src.y - self.y, src.x - self.x) game.level.map:particleEmitter(self.x, self.y, 1, "hit_warning", {angle=math.deg(angle)}) end end return ret end --- Can we continue running? -- We can run if no hostiles are in sight, and if no interesting terrain or characters are next to us. -- Known traps aren't interesting. We let the engine run around traps, or stop if it can't. -- 'ignore_memory' is only used when checking for paths around traps. This ensures we don't remember items "obj_seen" that we aren't supposed to function _M:runCheck(ignore_memory) if game:hasDialogUp(1) then return false, "dialog is displayed" end local spotted = spotHostiles(self, config.settings.marson.explore_telepathy) if #spotted > 0 then local dir = game.level.map:compassDirection(spotted[1].x - self.x, spotted[1].y - self.y) if game.player.running_prev then game.player.running_prev = nil end return false, ("hostile spotted to the %s (%s%s)"):format(dir or "???", spotted[1].actor.name, game.level.map:isOnScreen(spotted[1].x, spotted[1].y) and "" or " - offscreen") end if self.air_regen < 0 and self.air < 0.75 * self.max_air then return false, "losing breath!" end -- Notice any noticeable terrain local noticed = false self:runScan(function(x, y, what) -- Objects are always interesting, only on curent spot if what == "self" and not game.level.map.attrs(x, y, "obj_seen") then local obj = game.level.map:getObject(x, y, 1) if obj then if not ignore_memory then game.level.map.attrs(x, y, "obj_seen", true) end noticed = "object seen" return false, noticed end end local grid = game.level.map(x, y, Map.TERRAIN) if grid and grid.special and not grid.autoexplore_ignore and not game.level.map.attrs(x, y, "autoexplore_ignore") and self.running and self.running.path then game.level.map.attrs(x, y, "autoexplore_ignore", true) noticed = "something interesting" return false, noticed end -- Only notice interesting terrains, but allow auto-explore and A* to take us to the exit. Auto-explore can also take us through "safe" doors if grid and grid.notice and not (grid.special and self.running and self.running.explore and not grid.block_move and (grid.autoexplore_ignore or game.level.map.attrs(x, y, "autoexplore_ignore"))) and not (self.running and self.running.path and (game.level.map.attrs(x, y, "noticed") or (what ~= self and (self.running.explore and grid.door_opened -- safe door or #self.running.path == self.running.cnt and (self.running.explore == "exit" -- auto-explore onto exit or not self.running.explore and grid.change_level)) -- A* onto exit or #self.running.path - self.running.cnt < 2 and (self.running.explore == "portal" -- auto-explore onto portal or not self.running.explore and grid.orb_portal) -- A* onto portal or self.running.cnt < 3 and grid.orb_portal and -- path from portal game.level.map:checkEntity(self.running.path[1].x, self.running.path[1].y, Map.TERRAIN, "orb_portal")))) then -- let's not stop for open chests, activated pedestals, or farportal return portals - Marson if grid.chest_opened or grid.pedestal_activated or grid.change_zone == "shertul-fortress" or grid.change_zone == "shertul-fortress-2" then game.level.map.attrs(x, y, "autoexplore_ignore", true) elseif grid and grid.special then game.level.map.attrs(x, y, "autoexplore_ignore", true) noticed = "something interesting" elseif self.running and self.running.explore and self.running.path and self.running.explore ~= "unseen" and self.running.cnt == #self.running.path + 1 then noticed = "at " .. self.running.explore else noticed = "interesting terrain" game.level.map.attrs(x, y, "noticed", true) end -- let's only remember and ignore standard interesting terrain if not ignore_memory and (grid.change_level or grid.orb_portal or grid.escort_portal) then game.level.map.attrs(x, y, "noticed", true) end return false, noticed end if grid and grid.type and grid.type == "store" then noticed = "store entrance spotted" ; return false, noticed end -- Only notice interesting characters local actor = game.level.map(x, y, Map.ACTOR) if actor and actor.can_talk then noticed = "interesting character" ; return false, noticed end -- We let the engine take care of traps, but we should still notice "trap" stores. if game.level.map:checkAllEntities(x, y, "store") then noticed = "store entrance spotted" ; return false, noticed end end) if noticed then return false, noticed end return engine.interface.PlayerRun.runCheck(self) end --- Move with the mouse -- We just feed our spotHostile to the interface mouseMove function _M:mouseMove(tmx, tmy, force_move) local astar_check = function(x, y) -- Dont do traps local trap = game.level.map(x, y, Map.TRAP) if trap and trap:knownBy(self) and trap:canTrigger(x, y, self, true) then return false end -- Dont go where you cant breath if not self:attr("no_breath") then local air_level, air_condition = game.level.map:checkEntity(x, y, Map.TERRAIN, "air_level"), game.level.map:checkEntity(x, y, Map.TERRAIN, "air_condition") if air_level then if not air_condition or not self.can_breath[air_condition] or self.can_breath[air_condition] <= 0 then return false end end end return true end return engine.interface.PlayerMouse.mouseMove(self, tmx, tmy, function() local spotted = spotHostiles(self, config.settings.marson.explore_telepathy) ; return #spotted > 0 end, {recheck=true, astar_check=astar_check}, force_move) end --- Called after stopping running function _M:runStopped() game.level.map.clean_fov = true self:playerFOV() local spotted = spotHostiles(self, config.settings.marson.explore_telepathy) if #spotted > 0 then for _, node in ipairs(spotted) do node.actor:addParticles(engine.Particles.new("notice_enemy", 1)) end end -- if you stop at an object (such as on a trap), then mark it as seen local obj = game.level.map:getObject(x, y, 1) if obj then game.level.map.attrs(x, y, "obj_seen", true) end end return _M