-- TE4 - T-Engine 4 -- Copyright (C) 2009 - 2017 Nicolas Casalini -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see <http://www.gnu.org/licenses/>. -- -- Nicolas Casalini "DarkGod" -- darkgod@te4.org require "engine.class" local Entity = require "engine.Entity" local Map = require "engine.Map" local Faction = require "engine.Faction" --- Base Actor class used by NPCs, Players, Enemies, etc -- @classmod engine.Actor module(..., package.seeall, class.inherit(Entity)) --- Display actor when seen _M.display_on_seen = true --- Remember actor after seeing it _M.display_on_remember = false --- Display actor when it hasn't yet been seen _M.display_on_unknown = false -- Allow actors to act as object carriers, if the interface is loaded _M.__allow_carrier = true --- Instantiates an actor -- @param[type=table] t default values for actor -- @param[type=table] no_default override the default values for entities function _M:init(t, no_default) t = t or {} if not self.targetable and self.targetable == nil then self.targetable = true end self.name = t.name or "unknown actor" self.level = t.level or 1 self.sight = t.sight or 20 self.energy = t.energy or { value=0, mod=1 } self.energy.value = self.energy.value or 0 self.energy.mod = self.energy.mod or 0 self.faction = t.faction or "enemies" self.changed = true Entity.init(self, t, no_default) end --- Called when it is time to act -- @return true if alive function _M:act() if self.dead then return false end return true end --- Gets the actor target -- Does nothing, AI redefines it, so should a "Player" class function _M:getTarget() end --- Sets the actor target -- Does nothing, AI redefines it, so should a "Player" class function _M:setTarget(target) end --- cloneActor default alt_node fields (controls fields copied by cloneCustom) -- modules should update this as needed _M.clone_nodes = {player=false, x=false, y=false, fov_computed=false, fov={v={actors={}, actors_dist={}}}, distance_map={v={}}, _mo=false, _last_mo=false, add_mos=false, add_displays=false, shader=false, shader_args=false, } --- cloneActor default fields (merged by _M.cloneActor with cloneCustom) -- modules may define this as a table to automatically merge into cloned actors _M.clone_copy = nil --- Special version of cloneFull that clones an Actor, automatically managing duplication of some fields -- uses class.CloneCustom -- @param[optional, type=?table] post_copy a table merged into the cloned actor -- updated with self.clone_copy if it is defined -- @param[default=self.clone_nodes, type=?table] alt_nodes a table containing parameters for cloneCustom -- to be merged with self.clone_nodes -- @return the cloned actor function _M:cloneActor(post_copy, alt_nodes) alt_nodes = table.merge(alt_nodes or {}, self.clone_nodes, true) if post_copy or self.clone_copy then post_copy = post_copy or {} table.update(post_copy, self.clone_copy or {}, true) end local a = self:cloneCustom(alt_nodes, post_copy) a:removeAllMOs() return a, post_copy end --- Setup minimap color for this entity -- You may overload this method to customize your minimap -- @param mo -- @param[type=Map] map function _M:setupMinimapInfo(mo, map) if map.actor_player and not map.actor_player:canSee(self) then return end local r = map.actor_player and map.actor_player:reactionToward(self) or -100 if r < 0 then mo:minimap(240, 0, 0) elseif r > 0 then mo:minimap(0, 240, 0) else mo:minimap(0, 0, 240) end end --- Set the current emote -- @param[type=Emote] e function _M:setEmote(e) -- Remove previous if self.__emote then game.level.map:removeEmote(self.__emote) end self.__emote = e if e and self.x and self.y and game.level and game.level.map then e.x = self.x e.y = self.y game.level.map:addEmote(e) end end --- Attach or remove a display callback -- Defines particles to display function _M:defineDisplayCallback() if not self._mo then return end -- Cunning trick here! -- the callback we give to mo:displayCallback is a function that references self -- but self contains mo so it would create a cyclic reference and prevent GC'ing -- thus we store a reference to a weak table and put self into it -- this way when self dies the weak reference dies and does not prevent GC'ing local weak = setmetatable({[1]=self}, {__mode="v"}) local ps = self:getParticlesList() local f_self = nil local f_danger = nil local f_friend = nil local f_enemy = nil local f_neutral = nil local function particles(x, y, w, h) local self = weak[1] if not self or not self._mo then return end local e for i = 1, #ps do e = ps[i] e:checkDisplay() if e.ps:isAlive() then if game.level and game.level.map then e:shift(game.level.map, self._mo) end e.ps:toScreen(x + w / 2 + (e.dx or 0) * w, y + h / 2 + (e.dy or 0) * h, true, w / game.level.map.tile_w) elseif weak[1] then weak[1]:removeParticles(e) end end end local function tactical(x, y, w, h) -- Tactical info if game.level and game.level.map.view_faction then local self = weak[1] if not self then return end local map = game.level.map if not f_self then f_self = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_self) f_danger = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_danger) f_friend = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_friend) f_enemy = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_enemy) f_neutral = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_neutral) end if self.faction then local friend if not map.actor_player then friend = Faction:factionReaction(map.view_faction, self.faction) else friend = map.actor_player:reactionToward(self) end if self == map.actor_player then f_self:toScreen(x, y, w, h) elseif map:faction_danger_check(self) then f_danger:toScreen(x, y, w, h) elseif friend > 0 then f_friend:toScreen(x, y, w, h) elseif friend < 0 then f_enemy:toScreen(x, y, w, h) else f_neutral:toScreen(x, y, w, h) end end end end if self._mo == self._last_mo or not self._last_mo then self._mo:displayCallback(function(x, y, w, h) tactical(x, y, w, h) particles(x, y, w, h) return true end) else self._mo:displayCallback(function(x, y, w, h) tactical(x, y, w, h) return true end) self._last_mo:displayCallback(function(x, y, w, h) particles(x, y, w, h) return true end) end end --- Moves an actor on the map -- *WARNING*: changing x and y properties manually is *WRONG* and will blow up in your face. Use this method. Always. -- @int x coord of the destination -- @int y coord of the destination -- @param[type=boolean] force if true do not check for the presence of an other entity. *Use wisely* -- @return true if a move was *ATTEMPTED*. This means the actor will probably want to use energy function _M:move(x, y, force) if not x or not y then return end if self.dead then return true end if not game.level then return end local map = game.level.map x = math.floor(x) y = math.floor(y) if x < 0 then x = 0 end if x >= map.w then x = map.w - 1 end if y < 0 then y = 0 end if y >= map.h then y = map.h - 1 end if not force and map:checkAllEntities(x, y, "block_move", self, true) then return true end if self.x and self.y then map:remove(self.x, self.y, Map.ACTOR, self) else -- print("[MOVE] actor moved without a starting position", self.name, x, y) end self.old_x, self.old_y = self.x or x, self.y or y self.x, self.y = x, y map(x, y, Map.ACTOR, self) -- Move emote if self.__emote then if self.__emote.dead then self.__emote = nil else self.__emote.x = x self.__emote.y = y map.emotes[self.__emote] = true end end map:checkAllEntities(x, y, "on_move", self, force) return true end --- Moves into the given direction (calls `Actor:move`() internally) -- @number dir direction to move -- @number force amount to move -- @return true if we attempted to move function _M:moveDir(dir, force) local dx, dy = util.dirToCoord(dir, self.x, self.y) if dir ~= 5 then self.doPlayerSlide = config.settings.player_slide end -- Handles zig-zagging for non-square grids local zig_zag = util.dirZigZag(dir, self.x, self.y) local next_zig_zag = util.dirNextZigZag(dir, self.x, self.y) if next_zig_zag then -- in hex mode, {1,2,3,7,8,9} dirs self.zig_zag = next_zig_zag elseif zig_zag then -- in hex mode, {4,6} dirs self.zig_zag = self.zig_zag or "zig" local dir2 = zig_zag[self.zig_zag] dx, dy = util.dirToCoord(dir2, self.x, self.y) local nx, ny = util.coordAddDir(self.x, self.y, dir2) self.zig_zag = util.dirNextZigZag(self.zig_zag, nx, ny) if dir ~= 5 then self.doPlayerSlide = true end end local x, y = self.x + dx, self.y + dy self.move_dir = dir return self:move(x, y, force) end --- Can the actor go there -- @int x x coordinate -- @int y y coordinate -- @param[type=?boolean] terrain_only if true checks only the terrain, otherwise checks all entities -- @return true if it can function _M:canMove(x, y, terrain_only) if not game.level.map:isBound(x, y) then return false end if terrain_only then return not game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move", self) else return not game.level.map:checkAllEntities(x, y, "block_move", self) end end --- Remove the actor from the level, marking it as dead but not using the death functions -- @param src not used by default, but should be the event source function _M:disappear(src) if game.level and game.level:hasEntity(self) then game.level:removeEntity(self) end self.dead = true self.changed = true end --- Get the "path string" for this actor -- See `Map:addPathString`() for more info function _M:getPathString() return "" end --- Teleports randomly to a passable grid -- @int x the coord of the teleportation -- @int y the coord of the teleportation -- @number dist the radius of the random effect, if set to 0 it is a precise teleport -- @number min_dist the minimum radius of of the effect, will never teleport closer. Defaults to 0 if not set -- @return true if the teleport worked function _M:teleportRandom(x, y, dist, min_dist) local poss = {} dist = math.floor(dist) min_dist = math.floor(min_dist or 0) for i = x - dist, x + dist do for j = y - dist, y + dist do if game.level.map:isBound(i, j) and core.fov.distance(x, y, i, j) <= dist and core.fov.distance(x, y, i, j) >= min_dist and self:canMove(i, j) and not game.level.map.attrs(i, j, "no_teleport") then poss[#poss+1] = {i,j} end end end if #poss == 0 then return false end local pos = poss[rng.range(1, #poss)] return self:move(pos[1], pos[2], true) end --- Knock back the actor -- @int srcx source x -- @int srcy source y -- @number dist distance to push -- @param[type=?boolean] recursive is it recursive? -- @param[type=?boolean] on_terrain function _M:knockback(srcx, srcy, dist, recursive, on_terrain) print("[KNOCKBACK] from", srcx, srcx, "over", dist) local block_actor = function(_, bx, by) return game.level.map:checkEntity(bx, by, Map.TERRAIN, "block_move", self) end local l = core.fov.line(srcx, srcy, self.x, self.y, block_actor, true) local lx, ly, is_corner_blocked = l:step(true) local ox, oy = lx, ly dist = dist - 1 print("[KNOCKBACK] try", lx, ly, dist) if recursive then local target = game.level.map(lx, ly, Map.ACTOR) if target and recursive(target) then target:knockback(srcx, srcy, dist, recursive) end end if on_terrain then local g = game.level.map(lx, ly, Map.TERRAIN) if g and on_terrain(g, lx, ly) then dist = 0 end end while game.level.map:isBound(lx, ly) and not is_corner_blocked and not game.level.map:checkAllEntities(lx, ly, "block_move", self) and dist > 0 do dist = dist - 1 ox, oy = lx, ly lx, ly, is_corner_blocked = l:step(true) print("[KNOCKBACK] try", lx, ly, dist, "::", game.level.map:checkAllEntities(lx, ly, "block_move", self)) if recursive then local target = game.level.map(lx, ly, Map.ACTOR) if target and recursive(target) then target:knockback(srcx, srcy, dist, recursive) end end if on_terrain then local g = game.level.map(lx, ly, Map.TERRAIN) if g and on_terrain(g, lx, ly) then break end end end if game.level.map:isBound(lx, ly) and not game.level.map:checkAllEntities(lx, ly, "block_move", self) then print("[KNOCKBACK] ok knocked to", lx, ly, "::", game.level.map:checkAllEntities(lx, ly, "block_move", self)) self:move(lx, ly, true) elseif game.level.map:isBound(ox, oy) and not game.level.map:checkAllEntities(ox, oy, "block_move", self) then print("[KNOCKBACK] failsafe knocked to", ox, oy, "::", game.level.map:checkAllEntities(ox, oy, "block_move", self)) self:move(ox, oy, true) end end --- Pull the actor -- @int srcx source x -- @int srcy source y -- @number dist distance to pull -- @param[type=boolean] recursive is it recursive? function _M:pull(srcx, srcy, dist, recursive) print("[PULL] from", self.x, self.x, "towards", srcx, srcy, "over", dist) local block_actor = function(_, bx, by) return game.level.map:checkEntity(bx, by, Map.TERRAIN, "block_move", self) end local l = core.fov.line(self.x, self.y, srcx, srcy, block_actor) local lx, ly, is_corner_blocked = l:step() local ox, oy = lx, ly dist = dist - 1 print("[PULL] try", lx, ly, dist) if not lx or not ly then return end if recursive then local target = game.level.map(lx, ly, Map.ACTOR) if target and recursive(target) then target:pull(srcx, srcy, dist, recursive) end end while game.level.map:isBound(lx, ly) and not is_corner_blocked and not game.level.map:checkAllEntities(lx, ly, "block_move", self) and dist > 0 do dist = dist - 1 ox, oy = lx, ly lx, ly, is_corner_blocked = l:step() print("[PULL] try", lx, ly, dist, "::", game.level.map:checkAllEntities(lx, ly, "block_move", self)) if recursive then local target = game.level.map(lx, ly, Map.ACTOR) if target and recursive(target) then target:pull(srcx, srcy, dist, recursive) end end end if game.level.map:isBound(lx, ly) and not game.level.map:checkAllEntities(lx, ly, "block_move", self) then print("[PULL] ok pulled to", lx, ly, "::", game.level.map:checkAllEntities(lx, ly, "block_move", self)) self:move(lx, ly, true) elseif game.level.map:isBound(ox, oy) and not game.level.map:checkAllEntities(ox, oy, "block_move", self) then print("[PULL] failsafe pulled to", ox, oy, "::", game.level.map:checkAllEntities(ox, oy, "block_move", self)) self:move(ox, oy, true) end end --- Remove this actor from specified map -- @param[type=Map] map function _M:deleteFromMap(map) if self.x and self.y and map then map:remove(self.x, self.y, engine.Map.ACTOR, self) -- self.x, self.y = nil, nil self:closeParticles() end end --- Do we have enough energy? -- @number val -- @return true if we have enough function _M:enoughEnergy(val) val = val or game.energy_to_act return self.energy.value >= val end --- Use some energy -- @number val how much energy to use function _M:useEnergy(val) val = val or game.energy_to_act self.energy.value = self.energy.value - val self.energy.used = true if self.player and self.energy.value < game.energy_to_act then game.paused = false end -- print("USE ENERGY", self.name, self.uid, "::", self.energy.value, game.paused, "::", self.player) end --- What is our reaction toward the target -- @see Faction.factionReaction -- @param[type=Actor] target the target to check against function _M:reactionToward(target) return Faction:factionReaction(self.faction, target.faction) end --- Can the actor see the target actor -- This does not check LOS or such, only the actual ability to see it.<br/> -- By default this returns true, but a module can override it to check for telepathy, invisibility, stealth, ... -- @param[type=Actor] actor the target actor to check -- @number def the default -- @number def_pct the default percent chance -- @return[1] true -- @return[1] a number from 0 to 100 representing the "chance" to be seen -- @return[2] false -- @return[2] a number from 0 to 100 representing the "chance" to be seen function _M:canSee(actor, def, def_pct) return true, 100 end --- Create a line to target based on field of vision -- @int tx terrain x -- @int ty terrain y -- @param[type=?boolean|func|string] extra_block function that returns a boolean, or string that checks all entities to see if line of sight is blocked -- @param[type=?boolean] block boolean of whether or not it's blocked by default -- @int[opt=self.x] sx actor's x -- @int[opt=self.y] sy actor's y -- @return fov line from `core.fov.line` 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(tx, ty, Map.ACTOR) local sees_target = (self.sight and core.fov.distance(sx, sy, tx, ty) <= self.sight or not self.sight) and (game.level.map.lites(tx, ty) or act and self:canSee(act)) 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 sees_target and function(_, x, y) 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 extra_block and extra_block(self, x, y) end or function(_, x, y) if (self.sight and core.fov.distance(sx, sy, x, y) <= self.sight or not self.sight) and game.level.map.lites(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 extra_block and extra_block(self, x, y) else return true end end return core.fov.line(sx, sy, tx, ty, block) end --- Does the actor have LOS to the target -- @int x the spot we test for LOS -- @int y the spot we test for LOS -- @param[opt=block_sight] what the property to check for -- @int[opt=self.sight] range the maximum range to see -- @int[opt=self.x] source_x the spot to test from -- @int[opt=self.y] source_y the spot to test from -- @return[1] true -- @return[1] last_x -- @return[1] last_y -- @return[2] false -- @return[2] last_x -- @return[2] last_y function _M:hasLOS(x, y, what, range, source_x, source_y) source_x = source_x or self.x source_y = source_y or self.y if not x or not y then return false, source_x, source_y end what = what or "block_sight" range = range or self.sight local last_x, last_y = source_x, source_y local l = core.fov.line(source_x, source_y, x, y, what) local lx, ly, is_corner_blocked = l:step() -- Is within range, so no need to check every iteration if range and core.fov.distance(source_x, source_y, x, y) <= range then range = nil end while lx and ly and not is_corner_blocked do -- Check for the range if range and core.fov.distance(source_x, source_y, lx, ly) > range then break end last_x, last_y = lx, ly if game.level.map:checkAllEntities(lx, ly, what) then break end lx, ly, is_corner_blocked = l:step() end if last_x == x and last_y == y then return true, last_x, last_y end return false, last_x, last_y end --- Are we within a certain distance of the target -- @int x the spot we test for nearness -- @int y the spot we test for nearness -- @number radius how close we should be (defaults to 1) -- @return true if near function _M:isNear(x, y, radius) radius = radius or 1 if core.fov.distance(self.x, self.y, x, y) > radius then return false end return true end --- Return the kind of the entity -- @return "actor" function _M:getEntityKind() return "actor" end --- he/she formatting -- @return string.he_she(self) function _M:he_she() return string.he_she(self) end --- his/her formatting -- @return string.his_her(self) function _M:his_her() return string.his_her(self) end --- him/her formatting -- @return string.him_her(self) function _M:him_her() return string.him_her(self) end --- he/she/self formatting -- @return string.his_her_self(self) function _M:his_her_self() return string.his_her_self(self) end