Newer
Older
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org
require "engine.interface.PlayerMouse"
require "mod.class.interface.PlayerStats"
require "mod.class.interface.PlayerExplore"
require "mod.class.interface.PartyDeath"
local ActorTalents = require "engine.interface.ActorTalents"
--- Defines the player for ToME
-- It is a normal actor, with some redefined methods to handle user interaction.<br/>
module(..., package.seeall, class.inherit(
mod.class.Actor,
engine.interface.PlayerRest,
engine.interface.PlayerMouse,
engine.interface.PlayerSlide,
mod.class.interface.PlayerStats,
mod.class.interface.PlayerDumpJSON,
mod.class.interface.PlayerExplore,
mod.class.interface.PartyDeath
-- Allow character registration even after birth
allow_late_uuid = true
t.display=t.display or '@'
t.color_r=t.color_r or 230
t.color_g=t.color_g or 230
t.color_b=t.color_b or 230
t.unique = t.unique or "player"
if type(t.open_door) == "nil" then t.open_door = true end
t.type = t.type or "humanoid"
t.subtype = t.subtype or "player"
t.faction = t.faction or "players"
t.ai = t.ai or "tactical"
dg
committed
t.ai_state = t.ai_state or {talent_in=1, ai_move="move_astar"}
-- Dont give free resists & higher stat max to players
t.resists_cap = t.resists_cap or {}
t.old_air = 0
t.money_value_multiplier = t.money_value_multiplier or 1 -- changes amounts in gold piles and such
mod.class.Actor.init(self, t, no_default)
engine.interface.PlayerHotkeys.init(self, t)
self.descriptor = self.descriptor or {}
self.died_times = self.died_times or {}
self.last_learnt_talents = self.last_learnt_talents or { class={}, generic={} }
self.puuid = self.puuid or util.uuid()
self.damage_log = self.damage_log or {weapon={}}
self.damage_intake_log = self.damage_intake_log or {weapon={}}
self.talent_kind_log = self.talent_kind_log or {}
function _M:registerOnBirthForceWear(data)
self._on_birth = self._on_birth or {}
self._on_birth[#self._on_birth+1] = function()
local o = game.zone:makeEntityByName(game.level, "object", data, true)
o:identify(true)
local ro = self:wearObject(o, true, true)
if ro then
if type(ro) == "table" then self:addObject(self:getInven(self.INVEN_INVEN), ro) end
elseif not ro then
self:addObject(self:getInven(self.INVEN_INVEN), o)
end
end
end
function _M:registerOnBirth(f)
self._on_birth = self._on_birth or {}
self._on_birth[#self._on_birth+1] = f
end
function _M:onBirth(birther)
-- Make a list of random escort levels
local race_def = birther.birth_descriptor_def.race[self.descriptor.race]
local subrace_def = birther.birth_descriptor_def.subrace[self.descriptor.subrace]
local world = birther.birth_descriptor_def.world[self.descriptor.world]
local def = subrace_def.random_escort_possibilities or race_def.random_escort_possibilities
if world.random_escort_possibilities then def = world.random_escort_possibilities end -- World overrides
local zones = {}
for i, zd in ipairs(def) do for j = zd[2], zd[3] do zones[#zones+1] = {zd[1], j} end end
self.random_escort_levels = {}
for i = 1, world.random_escort_possibilities_max or 9 do
local z = rng.tableRemove(zones)
print("Random escort on", z[1], z[2])
self.random_escort_levels[z[1]] = self.random_escort_levels[z[1]] or {}
self.random_escort_levels[z[1]][z[2]] = true
end
end
for i, f in ipairs(self._on_birth or {}) do f(self, birther) end
self._on_birth = nil
end
function _M:onEnterLevel(zone, level)
self.entered_level = {x=self.x, y=self.y}
-- mark entrance (if applicable) as noticed
game.level.map.attrs(self.x, self.y, "noticed", true)
local escort_zone_name = zone.short_name
local escort_zone_offset = 0
if zone.tier1_escort then
escort_zone_offset = zone.tier1_escort - 1
self.entered_tier1_zones = self.entered_tier1_zones or {}
self.entered_tier1_zones.seen = self.entered_tier1_zones.seen or {}
self.entered_tier1_zones.nb = self.entered_tier1_zones.nb or 0
if not self.entered_tier1_zones.seen[zone.short_name] then
self.entered_tier1_zones.nb = self.entered_tier1_zones.nb + 1
self.entered_tier1_zones.seen[zone.short_name] = self.entered_tier1_zones.nb
end
escort_zone_name = "tier1."..self.entered_tier1_zones.seen[zone.short_name]
print("Entering tier1 zone for escort", escort_zone_name, escort_zone_offset, level.level - escort_zone_offset)
if self.random_escort_levels and self.random_escort_levels[escort_zone_name] then
table.print(self.random_escort_levels[escort_zone_name])
end
end
-- Fire random escort quest
if self.random_escort_levels and self.random_escort_levels[escort_zone_name] and self.random_escort_levels[escort_zone_name][level.level - escort_zone_offset] then
if self:triggerHook{"Player:onEnterLevel:generateEscort", zone=zone, level=level} then
-- nothing
elseif game:isCampaign("Maj'Eyal") then
self:grantQuest("escort-duty")
end
-- Cancel effects
local effs = {}
for eff_id, p in pairs(self.tmp) do
if self.tempeffect_def[eff_id].cancel_on_level_change then
effs[#effs+1] = eff_id
if type(self.tempeffect_def[eff_id].cancel_on_level_change) == "function" then self.tempeffect_def[eff_id].cancel_on_level_change(self, p) end
for i, eff_id in ipairs(effs) do self:removeEffect(eff_id, nil, true) end
-- Clear existing player created effects on the map
for i, eff in ipairs(level.map.effects) do
Chris Davidson
committed
if (eff.src and (eff.src.player or (eff.src.summoner and eff.src:resolveSource().player))) then
print("[onEnterLevel] Cancelling player created effect ", tostring(eff.name))
-- Clear existing player created entities from the map
local todel = {}
for uid, ent in pairs(level.entities) do
if ((ent.summoner and ent.summoner.player) or (ent.src and ent.src.player)) and not game.party:hasMember(ent) then
print("[onEnterLevel] Terminating player created entity ", uid, ent.name, ent:getEntityKind())
if ent.temporary then ent.temporary = 0 end
if ent.summon_time then ent.summon_time = 0 end
if ent.duration then ent.duration = 0 end
if ent:getEntityKind() == "projectile" then
todel[#todel+1] = ent
end
end
end
for _, ent in ipairs(todel) do
level:removeEntity(ent, true)
ent.dead = true
end
self:fireTalentCheck("callbackOnChangeLevel", "enter", zone, level)
function _M:onEnterLevelEnd(zone, level)
if level._player_enter_scatter then return end
level._player_enter_scatter = true
if level.data.generator and level.data.generator.map and (level.data.generator.map.class == "engine.generator.map.MapScript" or level.data.generator.map.class == "engine.generator.map.Static") and not level.data.static_force_scatter then return end
self:project({type="ball", radius=5}, self.x, self.y, function(px, py)
local a = level.map(px, py, Map.ACTOR)
if a and self:reactionToward(a) < 0 then
a:teleportRandom(self.x, self.y, 50, 5)
end
end)
end
function _M:onLeaveLevel(zone, level)
if self:hasEffect(self.EFF_FEED) then
self:removeEffect(self.EFF_FEED, true)
end
-- Fail past escort quests
local eid = "escort-duty-"..zone.short_name.."-"..level.level
if self:hasQuest(eid) and not self:hasQuest(eid):isEnded() then
local q = self:hasQuest(eid)
q.abandoned = true
self:setQuestStatus(eid, q.FAILED)
end
self:fireTalentCheck("callbackOnChangeLevel", "leave", zone, level)
-- Wilderness encounter
function _M:onWorldEncounter(target, x, y)
if x and y and game.level.map(x, y, Map.ACTOR) == target then
game.level.map:remove(x, y, Map.ACTOR)
end
function _M:describeFloor(x, y, force)
if self.old_x == x and self.old_y == y and not force then return end
Hachem_Muche
committed
-- Autopickup things on the floor
if self:getInven(self.INVEN_INVEN) and not self.no_inventory_access and not (self:attr("sleep") and not self:attr("lucid_dreamer")) then
local obj = game.level.map:getObject(x, y, i)
while obj do
if desc and self:attr("has_transmo") and obj.__transmo == nil then
if self:pickupFloor(i, true) then
desc = false
if self:transmoFilter(obj, self) then obj.__transmo = true end
if self:attr("auto_id") and obj:getPowerRank() <= self.auto_id then obj:identify(true) end
nb = nb + 1
game.logSeen(self, "There is an item here: %s", obj:getName{do_color=true})
end
obj = game.level.map:getObject(x, y, i)
end
end
local g = game.level.map(x, y, game.level.map.TERRAIN)
if g and g.change_level then
game.logPlayer(self, "#YELLOW_GREEN#There is %s here (press '<', '>' or right click to use).", g:getName():a_an())
local sx, sy = game.level.map:getTileToScreen(x, y, true)
game.flyers:add(sx, sy, 60, 0, -1.5, ("Level change (%s)!"):tformat(g:getName()), colors.simple(colors.YELLOW_GREEN), true)
function _M:openVault(vault_id)
local v = game.level.vaults_list[vault_id]
if not v then return end
print("Vault id", vault_id, "opens:", v.x, v.y, v.w, v.h)
for i = v.x, v.x + v.w - 1 do for j = v.y, v.y + v.h - 1 do
if game.level.map.attrs(i, j, "vault_id") == vault_id then
Eric Wykoff
committed
-- game.level.map.attrs(i, j, "vault_id", false)
local act = game.level.map(i, j, Map.ACTOR)
if act and not act.player then
act:removeEffect(act.EFF_VAULTED, true, true)
end
end
end end
end
if not force and self:enoughEnergy() and game.level.map:checkEntity(x, y, Map.TRAP, "is_store") then
game.level.map:checkEntity(x, y, Map.TRAP, "block_move", self, true)
return false
end
if not moved and self.encumbered then
game.logPlayer(self, "#FF0000#You carry too much--you are encumbered!")
game.logPlayer(self, "#FF0000#Drop some of your items.")
end
if not force and ox == self.x and oy == self.y and self.doPlayerSlide then
self.doPlayerSlide = nil
local tx, ty = self:tryPlayerSlide(x, y, false)
if tx then moved = self:move(tx, ty, false) end
end
self.doPlayerSlide = nil
dg
committed
game.level.map:moveViewSurround(self.x, self.y, config.settings.tome.scroll_dist, config.settings.tome.scroll_dist)
if self.describeFloor then self:describeFloor(self.x, self.y) end
if not force and game.level.map.attrs(self.x, self.y, "vault_id") and not game.level.map.attrs(self.x, self.y, "vault_only_door_open") then self:openVault(game.level.map.attrs(self.x, self.y, "vault_id")) end
-- if not force and ox == self.x and oy == self.y and self.tryPlayerSlide then
-- x, y = self:tryPlayerSlide(x, y, false)
-- self.tryPlayerSlide = false
-- moved = self:move(x, y, false)
-- self.tryPlayerSlide = nil
-- end
if game.zone.wilderness and not force then
if self.x ~= ox or self.y ~= oy then
game.state:worldDirectorAI()
end
-- Update zone name
if game.zone.variable_zone_name then game:updateZoneName() end
function _M:actBase()
mod.class.Actor.actBase(self)
-- Run out of time ?
if self.summon_time then
self.summon_time = self.summon_time - 1
if self.summon_time <= 0 then
game.logPlayer(self, "#PINK#Your summoned %s disappears.", self:getName())
end
function _M:act()
if not mod.class.Actor.act(self) then return end
-- Funky shader things !
self:updateMainShader()
local perc = (self.shader_old_life - self.life) / (self.max_life - self.die_at)
if perc > (config.settings.tome.life_lost_warning / 100) then
game.bignews:say(100, "#LIGHT_RED#LIFE LOST WARNING!")
game.key.disable_until = core.game.getTime() + 2000
game.mouse.disable_until = core.game.getTime() + 2000
end
end
self.old_air = self.air
self.old_healwarn = (self:attr("no_healing") or ((self.healing_factor or 1) <= 0))
-- game.flash:empty()
-- update feed/beckoned immediately before the player moves for best visual consistency (this is not perfect but looks much better than updating mid-move)
if self:hasEffect(self.EFF_FEED) then
self.tempeffect_def[self.EFF_FEED].updateFeed(self, self:hasEffect(self.EFF_FEED))
elseif self:hasEffect(self.EFF_FED_UPON) then
local fed_upon_eff = self:hasEffect(self.EFF_FED_UPON)
fed_upon_eff.src.tempeffect_def[fed_upon_eff.src.EFF_FEED].updateFeed(fed_upon_eff.src, fed_upon_eff.src:hasEffect(self.EFF_FEED))
end
Alex Ksandra
committed
if self.player and self:enoughEnergy() then
if self:restStep() then
while self:enoughEnergy() and self:restStep() do end
Alex Ksandra
committed
elseif self:runStep() then
while self:enoughEnergy() and self:runStep() do end
end
if self:enoughEnergy() then
Alex Ksandra
committed
game.paused = true
if game.uiset.logdisplay:getNewestLine() ~= "" then game.log("") end
end
elseif not self.player then
self:useEnergy()
function _M:useEnergy(val)
mod.class.Actor.useEnergy(self, val)
if self.player and self.energy.value < game.energy_to_act then
game.paused = false
self:fireTalentCheck("callbackOnActEnd")
end
end
function _M:tooltip(x, y, seen_by)
local str = mod.class.Actor.tooltip(self, x, y, seen_by)
if not str then return end
if config.settings.cheat then str:add(true, "UID: "..self.uid, true, self.image) end
return str
end
function _M:resetMainShader()
self.shader_old_life = nil
self.old_air = nil
self.old_psi = nil
self.old_healwarn = nil
self:updateMainShader()
end
--- Funky shader stuff
function _M:updateMainShader()
if game.fbo_shader then
DarkGod
committed
local effects = {}
DarkGod
committed
-- Set shader HP warning
if (self.life - self.die_at) < (self.max_life - self.die_at) / 2 then game.fbo_shader:setUniform("hp_warning", 1 - (self.life / (self.max_life - self.die_at)))
else game.fbo_shader:setUniform("hp_warning", 0) end
end
-- Set shader air warning
if self.air ~= self.old_air then
if self.air < self.max_air / 2 then game.fbo_shader:setUniform("air_warning", 1 - (self.air / self.max_air))
else game.fbo_shader:setUniform("air_warning", 0) end
end
if self:attr("solipsism_threshold") and self.psi ~= self.old_psi then
local solipsism_power = self:attr("solipsism_threshold") - self:getPsi()/self:getMaxPsi()
if solipsism_power > 0 then game.fbo_shader:setUniform("solipsism_warning", solipsism_power)
else game.fbo_shader:setUniform("solipsism_warning", 0) end
end
if ((self:attr("no_healing") or ((self.healing_factor or 1) <= 0)) ~= self.old_healwarn) and not self:attr("no_healing_no_warning") then
if (self:attr("no_healing") or ((self.healing_factor or 1) <= 0)) then
game.fbo_shader:setUniform("intensify", {0.3,1.3,0.3,1})
else
game.fbo_shader:setUniform("intensify", {0,0,0,0})
if self:attr("stealth") and self:attr("stealth") > 0 then game.fbo_shader:setUniform("colorize", {0.9,0.9,0.9,0.4})
elseif self:attr("invisible") and self:attr("invisible") > 0 then game.fbo_shader:setUniform("colorize", {0.3,0.4,0.9,0.3})
elseif self:attr("unstoppable") then game.fbo_shader:setUniform("colorize", {1,0.2,0,1})
elseif self:attr("lightning_speed") then game.fbo_shader:setUniform("colorize", {0.2,0.3,1,1})
elseif game.level and game.level.data.is_eidolon_plane then game.fbo_shader:setUniform("colorize", {1,1,1,1})
-- elseif game:hasDialogUp() then game.fbo_shader:setUniform("colorize", {0.9,0.9,0.9})
else game.fbo_shader:setUniform("colorize", {0,0,0,0}) -- Disable
if config.settings.tome.fullscreen_confusion and pf.blur and pf.blur.shad then
if self:attr("confused") and self.confused >= 1 then pf.blur.shad:uniBlur(2) effects[pf.blur.shad] = true
Eric Wykoff
committed
elseif self:attr("sleep") and not self:attr("lucid_dreamer") and self.sleep >= 1 then pf.blur.shad:uniBlur(2) effects[pf.blur.shad] = true
end
if pf.motionblur and pf.motionblur.shad then
if self:attr("invisible") then pf.motionblur.shad:uniMotionblur(3) effects[pf.motionblur.shad] = true
elseif self:attr("lightning_speed") then pf.motionblur.shad:uniMotionblur(2) effects[pf.motionblur.shad] = true
elseif game.level and game.level.data and game.level.data.motionblur then pf.motionblur.shad:uniMotionblur(game.level.data.motionblur) effects[pf.motionblur.shad] = true
end
-- Underwater shader
if game.level and game.level.data and game.level.data.underwater and pf.underwater and pf.underwater.shad then effects[pf.underwater.shad] = true
if config.settings.tome.fullscreen_stun and pf.wobbling and pf.wobbling.shad then
if self:attr("stunned") and self.stunned >= 1 then pf.wobbling.shad:uniWobbling(1) effects[pf.wobbling.shad] = true
elseif self:attr("dazed") and self.dazed >= 1 then pf.wobbling.shad:uniWobbling(0.7) effects[pf.wobbling.shad] = true
end
DarkGod
committed
-- Timestop shader
if self:attr("timestopping") and pf.timestop and pf.timestop.shad then
effects[pf.timestop.shad] = true
pf.timestop.shad:paramNumber("tick_start", core.game.getTime())
end
-- Sharpen shader
if config.settings.tome.sharpen_display and config.settings.tome.sharpen_display > 1 then
effects[pf.sharpen.shad] = true
pf.sharpen.shad:paramNumber("sharpen_power", config.settings.tome.sharpen_display)
end
DarkGod
committed
game.posteffects_use = table.keys(effects)
game.posteffects_use[#game.posteffects_use+1] = game.fbo_shader.shad
local fovdist = {}
for i = 0, 30 * 30 do
end
local wild_fovdist = {}
for i = 0, 10 * 10 do
wild_fovdist[i] = math.max((5 - math.sqrt(i)) / 1.4, 0.6)
end
-- Safety
if not self.x or not game.level then return end
-- Clean FOV before computing it
game.level.map:cleanFOV()
-- Do wilderness stuff, nothing else
if game.zone.wilderness then
self:computeFOV(game.zone.wilderness_see_radius, "block_sight", function(x, y, dx, dy, sqdist) game.level.map:applyLite(x, y, wild_fovdist[sqdist]) end, true, true, true)
return
end
if (self.esp_all and self.esp_all > 0) or next(self.esp) then
self:computeFOV(self.esp_range or 10, "block_esp", function(x, y) game.level.map:applyESP(x, y, 0.6) end, true, true, true)
end
-- Handle Sense spell, a simple FOV, using cache. Note that this means some terrain features can be made to block sensing
if self:attr("detect_range") then
self:computeFOV(self:attr("detect_range"), "block_sense", function(x, y)
local ok = false
if self:attr("detect_actor") and game.level.map(x, y, game.level.map.ACTOR) then ok = true end
if self:attr("detect_object") and game.level.map(x, y, game.level.map.OBJECT) then ok = true end
if self:attr("detect_trap") and game.level.map(x, y, game.level.map.TRAP) then
Hachem_Muche
committed
game.level.map(x, y, game.level.map.TRAP):setKnown(self, true, x, y)
game.level.map.remembers(x, y, true)
Chris Davidson
committed
-- See everything and ignore all forms of blocking, dev mode feature
if self:attr("omnivision") then
self:computeFOV(self:attr("omnivision"), "we_need_useless_string_not_nil", function(x, y)
local ok = false
if game.level.map(x, y, game.level.map.ACTOR) then ok = true end
if game.level.map(x, y, game.level.map.OBJECT) then ok = true end
if game.level.map(x, y, game.level.map.TRAP) then
game.level.map(x, y, game.level.map.TRAP):setKnown(self, true, x, y)
game.level.map.remembers(x, y, true)
game.level.map:updateMap(x, y)
ok = true
end
if ok then
game.level.map.seens(x, y, 0.6)
end
end, true, true, true)
end
-- Handle arcane eye
if self:hasEffect(self.EFF_ARCANE_EYE) then
local eff = self:hasEffect(self.EFF_ARCANE_EYE)
local map = game.level.map
core.fov.calc_circle(
eff.x, eff.y, game.level.map.w, game.level.map.h, eff.radius, function(_, x, y) if map:checkAllEntities(x, y, "block_sight", self) then return true end end,
function(_, x, y)
local t = map(x, y, map.ACTOR)
if t and (eff.true_seeing or self:canSee(t)) then
map.seens(x, y, 1)
if self.can_see_cache[t] then self.can_see_cache[t]["nil/nil"] = {true, 100} end
if t ~= self then t:setEffect(t.EFF_ARCANE_EYE_SEEN, 1, {src=self, true_seeing=eff.true_seeing}) end
end,
cache and map._fovcache["block_sight"]
)
end
core.fov.calc_circle(self.x, self.y, game.level.map.w, game.level.map.h, 10,
function(d, x, y)end, -- block
function(d, x, y) -- apply
local act = game.level.map(x, y, game.level.map.ACTOR)
if act then
local eff = act:hasEffect(act.EFF_MARKED)
if eff and eff.src==self then
game.level.map.seens(x, y, 0.6)
end
end
end,
nil)
-- Handle Preternatural Senses talent, a simple FOV, using cache.
if self:knowTalent(self.T_PRETERNATURAL_SENSES) then
local t = self:getTalentFromId(self.T_PRETERNATURAL_SENSES)
local range = self:getTalentRange(t)
self:computeFOV(range, "block_sense", function(x, y)
if game.level.map(x, y, game.level.map.ACTOR) then
local effStalker = self:hasEffect(self.EFF_STALKER)
if effStalker then
if core.fov.distance(self.x, self.y, effStalker.target.x, effStalker.target.y) <= 10 then
game.level.map.seens(effStalker.target.x, effStalker.target.y, 0.6)
end
end
if self:hasEffect(self.EFF_PREDATOR) then
local uid, e = next(game.level.entities)
while uid do
if e.marked_prey then
if self:knowTalent(self.T_SHADOW_SENSES) then
local t = self:getTalentFromId(self.T_SHADOW_SENSES)
local range = self:getTalentRange(t)
local sqsense = range * range
for shadow, _ in pairs(game.party.members) do if shadow.is_doomed_shadow and not shadow.dead and shadow.x then
local arr = shadow.fov.actors_dist
local tbl = shadow.fov.actors
local act
game.level.map:apply(shadow.x, shadow.y, 0.6)
game.level.map:applyExtraLite(shadow.x, shadow.y)
for i = 1, #arr do
act = arr[i]
if act and not act.dead and act.x and tbl[act] and shadow:canSee(act) and tbl[act].sqdist <= sqsense then
game.level.map.seens(act.x, act.y, 0.6)
end
end
if self:knowTalent(self.T_SPECTRAL_SIGHT) then
local t = self:getTalentFromId(self.T_SPECTRAL_SIGHT)
local range = self:getTalentRange(t)
local sqsense = range * range
for minion, _ in pairs(game.party.members) do if minion.necrotic_minion and not minion.dead and minion.x then
local arr = minion.fov.actors_dist
local tbl = minion.fov.actors
local act
game.level.map:apply(minion.x, minion.y, 0.6)
game.level.map:applyExtraLite(minion.x, minion.y)
for i = 1, #arr do
act = arr[i]
if act and not act.dead and act.x and tbl[act] and minion:canSee(act) and tbl[act].sqdist <= sqsense then
game.level.map.seens(act.x, act.y, 0.6)
end
end
end end
end
if not self:attr("blind") then
-- Handle infravision/heightened_senses which allow to see outside of lite radius but with LOS
-- Note: Overseer of Nations bonus already factored into attributes
if self:attr("infravision") or self:attr("heightened_senses") then
local radius = math.max((self.heightened_senses or 0), (self.infravision or 0))
radius = math.min(radius, self.sight)
local rad2 = math.max(1, math.floor(radius / 4))
self:computeFOV(radius, "block_sight", function(x, y, dx, dy, sqdist) if game.level.map(x, y, game.level.map.ACTOR) then game.level.map.seens(x, y, fovdist[sqdist]) end end, true, true, true)
self:computeFOV(rad2, "block_sight", function(x, y, dx, dy, sqdist) game.level.map:applyLite(x, y, fovdist[sqdist]) end, true, true, true)
end
-- Compute both the normal and the lite FOV, using cache
-- Do it last so it overrides others
self:computeFOV(self.sight or 10, "block_sight", function(x, y, dx, dy, sqdist)
game.level.map:apply(x, y, fovdist[sqdist])
end, true, false, true)
local lradius = self.lite
if self.radiance_aura and lradius < self.radiance_aura then lradius = self.radiance_aura end
if self.lite <= 0 then game.level.map:applyLite(self.x, self.y)
Chris Davidson
committed
else
self:computeFOV(lradius, "block_sight", function(x, y, dx, dy, sqdist)
game.level.map:applyExtraLite(x, y)
end,
true, true, true)
end
-- For each entity, generate lite
local uid, e = next(game.level.entities)
while uid do
if e ~= self and ((e.lite and e.lite > 0) or (e.radiance_aura and e.radiance_aura > 0)) and e.computeFOV then
e:computeFOV(math.max(e.lite or 0, e.radiance_aura or 0), "block_sight", function(x, y, dx, dy, sqdist) game.level.map:applyExtraLite(x, y, fovdist[sqdist]) end, true, true)
end
uid, e = next(game.level.entities, uid)
end
else
self:computeFOV(self.sight or 10, "block_sight") -- Still compute FOV so NPCs may target us even while blinded
-- Inner Sight; works even while blinded
if self:attr("blind_sight") then
self:computeFOV(self:attr("blind_sight"), "block_sight", function(x, y, dx, dy, sqdist) game.level.map:applyLite(x, y, 0.6) end, true, true, true)
self:postFOVCombatCheck()
--- Create a line to target based on field of vision
function _M:lineFOV(tx, ty, extra_block, block, sx, sy)
sx = sx or self.x
sy = sy or self.y
local act = game.level.map(x, y, Map.ACTOR)
local sees_target = game.level.map.seens(tx, ty)
extra_block = type(extra_block) == "function" and extra_block
or type(extra_block) == "string" and function(_, x, y) return game.level.map:checkAllEntities(x, y, extra_block) end
block = block or function(_, x, y)
if sees_target then
return game.level.map:checkAllEntities(x, y, "block_sight") or
game.level.map:checkEntity(x, y, engine.Map.TERRAIN, "block_move") and not game.level.map:checkEntity(x, y, engine.Map.TERRAIN, "pass_projectile") or
elseif core.fov.distance(sx, sy, x, y) <= self.sight and (game.level.map.remembers(x, y) or game.level.map.seens(x, y)) then
return game.level.map:checkEntity(x, y, Map.TERRAIN, "block_sight") or
game.level.map:checkEntity(x, y, engine.Map.TERRAIN, "block_move") and not game.level.map:checkEntity(x, y, engine.Map.TERRAIN, "pass_projectile") or
else
return true
end
end
return core.fov.line(sx, sy, tx, ty, block)
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(_t"taken damage")
self:restStop(_t"taken damage")
local ret = mod.class.Actor.onTakeHit(self, value, src, death_note)
if self.life < self.max_life * 0.3 then
local sx, sy = game.level.map:getTileToScreen(self.x, self.y, true)
game.flyers:add(sx, sy, 30, (rng.range(0,2)-1) * 0.5, 2, _t"LOW HEALTH!", {255,0,0}, true)
-- 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
function _M:on_set_temporary_effect(eff_id, e, p)
local ret = mod.class.Actor.on_set_temporary_effect(self, eff_id, e, p)
if e.status == "detrimental" and not e.no_stop_resting and p.dur > 0 then
self:runStop(_t"detrimental status effect")
self:restStop(_t"detrimental status effect")
function _M:heal(value, src)
-- Difficulty settings
if game.difficulty == game.DIFFICULTY_EASY then
value = value * 1.3
if self.runStop then self:runStop(_t"died") end
if self.restStop then self:restStop(_t"died") end
dg
committed
function _M:suffocate(value, src, death_msg)
local dead, affected = mod.class.Actor.suffocate(self, value, src, death_msg)
if affected and value > 0 and self.runStop then
-- only stop autoexplore when air is less than 75% of max.
if self.air < 0.75 * self.max_air and self.air < 100 then
self:runStop(_t"suffocating")
self:restStop(_t"suffocating")
self:runStop(_t"chat started")
self:restStop(_t"chat started")
--- Notify the player of available cooldowns
function _M:onTalentCooledDown(tid)
local x, y = game.level.map:getTileToScreen(self.x, self.y, true)
game.flyers:add(x, y, 30, -0.3, -3.5, ("%s available"):tformat(t.name:capitalize()), {0,255,00})
game.log("#00ff00#%sTalent %s is ready to use.", (t.display_entity and t.display_entity:getDisplayString() or ""), t.name)
if self:attr("encased_in_ice") then
if type(typ) ~= "table" then
return self.x, self.y, self
end
local orig_range = typ.range
typ.range = 0
local x, y, act = game:targetGetForPlayer(typ)
typ.range = orig_range
if x then
return self.x, self.y, self
else
return
end
else
if type(typ) == "table" and typ.range and typ.range == 1 and config.settings.tome.immediate_melee_keys then
local oldft = typ.first_target
typ = table.clone(typ)
typ.first_target = "friend"
typ.immediate_keys = true
typ.default_target = self
if config.settings.tome.immediate_melee_keys_auto and not oldft and not typ.simple_dir_request then
local foes = {}
for _, c in pairs(util.adjacentCoords(self.x, self.y)) do
local target = game.level.map(c[1], c[2], Map.ACTOR)
if target and self:reactionToward(target) < 0 then foes[#foes+1] = target end
end
if #foes == 1 then
game.target.target.entity = foes[1]
game.target.target.x = foes[1].x
game.target.target.y = foes[1].y
return game.target.target.x, game.target.target.y, game.target.target.entity
end
end
return game:targetGetForPlayer(typ)
end
--- Sets the current target
function _M:setTarget(target)
return game:targetSetForPlayer(target)
local function spotHostiles(self, actors_only)
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)
dg
committed
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:getName()}
dg
committed
end
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
_M.spotHostiles = spotHostiles
--- Try to auto use listed talents
-- This should be called in your actors "act()" method
function _M:automaticTalents()
if self.no_automatic_talents then return end
local uses = {}
for tid, c in pairs(self.talents_auto) do
local t = self.talents_def[tid]
local spotted = spotHostiles(self, true)
local cd = self:getTalentCooldown(t) or (t.is_object_use and t.cycle_time(self, t)) or 0
local turns_used = util.getval(t.no_energy, self, t) == true and 0 or 1
if cd <= turns_used and t.mode ~= "sustained" then
game.logPlayer(self, "Automatic use of talent %s #DARK_RED#skipped#LAST#: cooldown too low (%d).", self:getTalentDisplayName(t), cd)
Alex Ksandra
committed
elseif (t.mode ~= "sustained" or not self.sustain_talents[tid]) and not self.talents_cd[tid] and self:preUseTalent(t, true, true) and (not t.auto_use_check or t.auto_use_check(self, t)) then
if (c == 1) or (c == 2 and #spotted <= 0) or (c == 3 and #spotted > 0) or (c == 5 and not self.in_combat) then
if c ~= 2 then
Alex Ksandra
committed
uses[#uses+1] = {name=t.name, turns_used=turns_used, cd=cd, fct=function() self:useTalent(tid) end}
if not self:attr("blind") then
Alex Ksandra
committed
uses[#uses+1] = {name=t.name, turns_used=turns_used, cd=cd, fct=function() self:useTalent(tid,nil,nil,nil,self) end}
end
end
if c == 4 and #spotted > 0 then
for fid, foe in pairs(spotted) do
if foe.x >= self.x-1 and foe.x <= self.x+1 and foe.y >= self.y-1 and foe.y <= self.y+1 then
Alex Ksandra
committed
uses[#uses+1] = {name=t.name, turns_used=turns_used, cd=cd, fct=function() self:useTalent(tid) end}
end
end
end
end
end
table.sort(uses, function(a, b)
local an, nb = a.turns_used, b.turns_used
if an < nb then return true
elseif an > nb then return false
else
if a.cd > b.cd then return true
else return false
end
end
end)
for _, use in ipairs(uses) do
use.fct()
Alex Ksandra
committed
if use.turns_used > 0 then break end
end
--- We started resting