diff --git a/game/engines/default/engine/interface/ActorAI.lua b/game/engines/default/engine/interface/ActorAI.lua index ba2f754c4d98973526e6ab53311831484c64ff6d..8c88d41165b3736125555fa9730f378b4eb48d0f 100644 --- a/game/engines/default/engine/interface/ActorAI.lua +++ b/game/engines/default/engine/interface/ActorAI.lua @@ -123,7 +123,7 @@ function _M:doAI() local target_pos = self.ai_target.actor and self.fov and self.fov.actors and self.fov.actors[self.ai_target.actor] if target_pos then local tx, ty = self:aiSeeTargetPos(self.ai_target.actor) - self.ai_state.target_last_seen = {x=tx, y=ty, turn=self.fov_last_turn} + self.ai_state.target_last_seen=table.merge(self.ai_state.target_last_seen or {}, {x=tx, y=ty, turn=self.fov_last_turn}) -- Merge to keep obfuscation data end return self:runAI(self.ai) @@ -148,39 +148,43 @@ function _M:setTarget(target, last_seen) self.ai_state.target_last_seen = last_seen else local target_pos = target and self.fov and self.fov.actors and self.fov.actors[self.ai_target.actor] or {x=self.x, y=self.y} - self.ai_state.target_last_seen = {x=target_pos.x, y=target_pos.y, turn=game.turn} + self.ai_state.target_last_seen=table.merge(self.ai_state.target_last_seen or {}, {x=target_pos.x, y=target_pos.y, turn=game.turn}) -- Merge to keep obfuscation data end end --- Returns the seen coords of the target -- This will usually return the exact coords, but if the target is only partially visible (or not at all) --- it will return estimates, to throw the AI a bit off +-- it will return estimates, to throw the AI a bit off (up to 10 tiles error) -- @param target the target we are tracking -- @return x, y coords to move/cast to function _M:aiSeeTargetPos(target) if not target then return self.x, self.y end local tx, ty = target.x, target.y + local LSeen = self.ai_state.target_last_seen + if type(LSeen) ~= "table" then return tx, ty end local spread = 0 + LSeen.GCache_turn = LSeen.GCache_turn or game.turn -- Guess Cache turn to update position guess (so it's consistent during a turn) + LSeen.GCknown_turn = LSeen.GCknown_turn or game.turn -- Guess Cache known turn for spread calculation (self.ai_state.target_last_seen.turn can't be used because it's needed by FOV code) - -- Adding some type-safety checks, but this isn't fixing the source of the errors - if target == self.ai_target.actor and self.ai_state.target_last_seen and type(self.ai_state.target_last_seen) == "table" and self.ai_state.target_last_seen.x and not self:hasLOS(self.ai_state.target_last_seen.x, self.ai_state.target_last_seen.y) then - tx, ty = self.ai_state.target_last_seen.x, self.ai_state.target_last_seen.y - spread = spread + math.floor((game.turn - (self.ai_state.target_last_seen.turn or game.turn)) / (game.energy_to_act / game.energy_per_tick)) - end - + -- Check if target is currently seen local see, chance = self:canSee(target) - - -- Compute the maximum spread if we need to obfuscate - local spread = see and 0 or math.floor((100 - chance) / 10) - - - -- We don't know the exact position, so we obfuscate - if spread > 0 then - tx = tx + rng.range(0, spread * 2) - spread - ty = ty + rng.range(0, spread * 2) - spread - return util.bound(tx, 0, game.level.map.w - 1), util.bound(ty, 0, game.level.map.h - 1) - -- Directly seeing it, no spread at all + if see and self:hasLOS(target.x, target.y) then -- canSee doesn't check LOS + LSeen.GCache_x, LSeen.GCache_y = nil, nil + LSeen.GCknown_turn = game.turn + LSeen.GCache_turn = game.turn else - return util.bound(tx, 0, game.level.map.w - 1), util.bound(ty, 0, game.level.map.h - 1) + if target == self.ai_target.actor and (LSeen.GCache_turn or 0) + 10 <= game.turn and LSeen.x then + spread = spread + math.min(10, math.floor((game.turn - (LSeen.GCknown_turn or game.turn)) / (game.energy_to_act / game.energy_per_tick))) -- Limit spread to 10 tiles + tx, ty = util.bound(tx + rng.range(-spread, spread), 0, game.level.map.w - 1), util.bound(ty + rng.range(-spread, spread), 0, game.level.map.h - 1) + -- Inertial average with last guess: can specify another method here to make the targeting position less random + if LSeen.GCache_x then -- update guess with new random position. Could use util.findFreeGrid here at cost of speed + tx = math.floor(LSeen.GCache_x + (tx-LSeen.GCache_x)/2) + ty = math.floor(LSeen.GCache_y + (ty-LSeen.GCache_y)/2) + end + LSeen.GCache_x, LSeen.GCache_y = tx, ty + LSeen.GCache_turn = game.turn + end + if LSeen.GCache_x then return LSeen.GCache_x, LSeen.GCache_y end end -end + return tx, ty -- Fall through to correct coords +end \ No newline at end of file diff --git a/game/modules/tome/class/GameState.lua b/game/modules/tome/class/GameState.lua index 581e67f85e1b638e80e0ba0c2db4e9641adfbc48..0a055641a247c8e708709338174feec56b1ba95b 100644 --- a/game/modules/tome/class/GameState.lua +++ b/game/modules/tome/class/GameState.lua @@ -355,6 +355,7 @@ function _M:generateRandart(data) -- Add ego properties ----------------------------------------------------------- local nb_egos = data.egos or 3 + local gr_egos = data.greater_egos_bias or math.floor(nb_egos*2/3) -- 2/3 greater egos by default if o.egos and nb_egos > 0 then local legos = {} local been_greater = 0 @@ -365,7 +366,7 @@ function _M:generateRandart(data) local egos = rng.table(legos) local list = {} local filter = nil - if rng.percent(lev) and been_greater < 2 then been_greater = been_greater + 1 filter = function(e) return e.greater_ego end end + if rng.percent(100*lev/(lev+50)) and been_greater < gr_egos then been_greater = been_greater + 1 filter = function(e) return e.greater_ego end end --RE Phase out (but don't eliminate) lesser egos with level for z = 1, #egos do list[#list+1] = egos[z].e end local ef = self:egoFilter(game.zone, game.level, "object", "randartego", o, {special=filter, forbid_power_source=data.forbid_power_source, power_source=data.power_source}, list, {}) @@ -1266,6 +1267,32 @@ function _M:entityFilterPost(zone, level, type, e, filter) end if data.user_post then data.user_post(b, data) end end, + post = function(b, data) + if data.level <= 20 then + b.inc_damage = b.inc_damage or {} + b.inc_damage.all = (b.inc_damage.all or 0) - 40 * (20 - data.level + 1) / 20 + end + + -- Drop + for i = 1, data.nb_rares do -- generate rares as weak (1 ego) randarts + local fil = {lev=lev, egos=1, greater_egos_bias = 0, forbid_power_source=b.not_power_source, + base_filter = {no_tome_drops=true, ego_filter={keep_egos=true, ego_chance=-1000}, + special=function(e) + return (not e.unique and e.randart_able) and (not e.material_level or e.material_level >= 1) and true or false + end} + } + local o = game.state:generateRandart(fil,nil, true) + if o then +-- print("[entityFilterPost]: Generated random object for", tostring(b.name)) --RE + o.unique, o.randart, o.rare = nil, nil, true + b:addObject(b.INVEN_INVEN, o) + game.zone:addEntity(game.level, o, "object") + else + print("[entityFilterPost]: Failed to generate random object for", tostring(b.name)) + end + end + if data.user_post then data.user_post(b, data) end + end, } e = self:createRandomBoss(e, table.merge(base, filter.random_elite, true)) end @@ -1273,6 +1300,8 @@ function _M:entityFilterPost(zone, level, type, e, filter) if filter.random_object and not e.unique and e.randart_able then local data = _G.type(filter.random_object) == "table" and filter.random_object or {} local lev = math.max(1, game.zone:level_adjust_level(game.level, game.zone, "object")) + print("[entityFilterPost]: Generating obsolete random_object") + print(debug.traceback()) e = game.state:generateRandart{ lev = lev, egos = 0, diff --git a/game/modules/tome/class/NPC.lua b/game/modules/tome/class/NPC.lua index be6ebd9468f1601c24d7b77ba0b7fea644cc724a..f3e6edbe59f52f773517ebf0d5dc079f97a48fe2 100644 --- a/game/modules/tome/class/NPC.lua +++ b/game/modules/tome/class/NPC.lua @@ -409,14 +409,28 @@ function _M:tooltip(x, y, seen_by) local str = mod.class.Actor.tooltip(self, x, y, seen_by) if not str then return end local killed = game:getPlayer(true).all_kills and (game:getPlayer(true).all_kills[self.name] or 0) or 0 - + local target = self.ai_target.actor + str:add( true, ("Killed by you: %s"):format(killed), true, - "Target: ", self.ai_target.actor and self.ai_target.actor.name or "none" + "Target: ", target and target.name or "none" ) - if config.settings.cheat then str:add(true, "UID: "..self.uid, true, self.image) end - + -- Give hints to stealthed/invisible players about where the NPC is looking (if they have LOS) + if target == game.player and (game.player:attr("stealth") or game.player:attr("invisible")) and game.player:hasLOS(self.x, self.y) then + local tx, ty = self:aiSeeTargetPos(self.ai_target.actor) + local dx, dy = tx - self.ai_target.actor.x, ty - self.ai_target.actor.y + local offset = engine.Map:compassDirection(dx, dy) + if offset then + str:add(" looking " ..offset) + if config.settings.cheat then str:add((" (%+d, %+d)"):format(dx, dy)) end + else + str:add(" looking at you.") + end + end + if config.settings.cheat then + str:add(true, "UID: "..self.uid, true, self.image) + end return str end diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua index f6224d44c7e9fec1cde6c834fc0498a8c088c30a..216ffe6468de26a3efe307850edfe43e1d62ec61 100644 --- a/game/modules/tome/class/Player.lua +++ b/game/modules/tome/class/Player.lua @@ -154,6 +154,14 @@ function _M:onEnterLevel(zone, level) end end for i, eff_id in ipairs(effs) do self:removeEffect(eff_id) end + + -- Clear existing player created effects on the map + for i, eff in ipairs(level.map.effects) do + if eff.src and eff.src == game.player then + eff.duration = 0 + eff.grids = {} + end + end end function _M:onEnterLevelEnd(zone, level) diff --git a/game/modules/tome/data/talents/cunning/traps.lua b/game/modules/tome/data/talents/cunning/traps.lua index cdb5f53cb510e34b861d4032f1b66705eb2789e6..881f45ff9f16f0ff1ca90341f8bf1a6814b1e99a 100644 --- a/game/modules/tome/data/talents/cunning/traps.lua +++ b/game/modules/tome/data/talents/cunning/traps.lua @@ -648,6 +648,7 @@ newTalent{ if not x or not y then return nil end local _ _, x, y = self:canProject(tg, x, y) if game.level.map(x, y, Map.TRAP) then game.logPlayer(self, "You somehow fail to set the trap.") return nil end + if game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") then game.logPlayer(self, "You somehow fail to set the trap.") return nil end local dam = t.getDamage(self, t) -- Need to pass the actor in to the triggered function for the apply_power to work correctly diff --git a/game/modules/tome/data/talents/misc/races.lua b/game/modules/tome/data/talents/misc/races.lua index e97c39f47acdbc251eb4a117706f2b3e60be91bc..37bd92e09480a97f5315a909af3237534ff3014c 100644 --- a/game/modules/tome/data/talents/misc/races.lua +++ b/game/modules/tome/data/talents/misc/races.lua @@ -358,7 +358,7 @@ newTalent{ local x, y = util.findFreeGrid(tx, ty, 5, true, {[Map.ACTOR]=true}) if not x then game.logPlayer(self, "Not enough space to summon!") - return + if i == 1 then return else break end end local NPC = require "mod.class.NPC" diff --git a/game/modules/tome/data/talents/spells/aether.lua b/game/modules/tome/data/talents/spells/aether.lua index e7e152f578c3baa651b2deab65bc5ad8826edd08..a783f57458212696035e626b1a0815b5761dfcbf 100644 --- a/game/modules/tome/data/talents/spells/aether.lua +++ b/game/modules/tome/data/talents/spells/aether.lua @@ -62,6 +62,7 @@ newTalent{ if not x or not y then return nil end local _ _, x, y = self:canProject(tg, x, y) if game.level.map(x, y, Map.TRAP) then game.logPlayer(self, "You somehow fail to set the aether beam.") return nil end + if game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") then game.logPlayer(self, "You somehow fail to set the aether beam.") return nil end local t = basetrap(self, t, x, y, 44, { type = "aether", name = "aether beam", color=colors.VIOLET, image = "trap/trap_glyph_explosion_01_64.png",