feat: add canSee and isAlly
This commit is contained in:
1 parent
1f4ea71a1f
commit
2faa8dbe75
1 file changed
+110
@@ -0,0 +1,110 @@
|
||||
-- Monsters that share no faction bond will occasionally fight each other
|
||||
-- outright instead of always beelining for the player. Two rival factions
|
||||
-- (see Factions.RIVALRIES) throw down on sight, and any monster carrying
|
||||
-- Status.DISCORD - landed by the discord spell, see spells.lua - is
|
||||
-- compelled to strike whatever creature stands nearest it, friend or foe,
|
||||
-- for the effect's duration. Either way the fight runs through combat.lua
|
||||
-- exactly like a player's own attack - same damage formula, same
|
||||
-- resistances, same Game:applyEliteOnHit hooks (an elite attacker's
|
||||
-- lifesteal or on-hit status rider fires here too, not just against the
|
||||
-- player) and the same disease.lua bite chance (a plague-carrier's bite can
|
||||
-- infect a rival monster or the player's own companion/hireling/thrall,
|
||||
-- not just the player Game:monsterMelee already covers) - so a discorded
|
||||
-- troll among goblins is a real threat to both sides, not a scripted
|
||||
-- distraction. A plain monster felled this way
|
||||
-- still leaves a corpse (scavenging/reanimation can use it) but pays out no
|
||||
-- xp, essence, faction standing, or compendium credit - the player didn't
|
||||
-- land the blow. A companion, hireling, or thrall killed this way (only
|
||||
-- reachable through discord, since none of them carry a faction) dies
|
||||
-- exactly as it would to any other monster.
|
||||
|
||||
local Combat = require("combat")
|
||||
local Status = require("status")
|
||||
local Factions = require("factions")
|
||||
local Los = require("los")
|
||||
local Corpses = require("corpses")
|
||||
local Noise = require("noise")
|
||||
local Disease = require("disease")
|
||||
|
||||
local Infighting = {}
|
||||
|
||||
Infighting.SIGHT_RANGE = 8
|
||||
|
||||
local function canSee(game, a, b)
|
||||
local cheb = math.max(math.abs(a.x - b.x), math.abs(a.y - b.y))
|
||||
if cheb > Infighting.SIGHT_RANGE then return false end
|
||||
return Los.clear(game.level.map, a.x, a.y, b.x, b.y)
|
||||
end
|
||||
|
||||
local function isAlly(actor)
|
||||
return actor.kind == "companion" or actor.kind == "hireling" or actor.kind == "thrall"
|
||||
end
|
||||
|
||||
-- The other living, non-player creature `monster` should be fighting this
|
||||
-- turn instead of following its usual Ai.act behaviour, or nil if none
|
||||
-- qualifies. A rival-faction monster always qualifies once seen; under
|
||||
-- Status.DISCORD, the nearest visible creature of any kind - including the
|
||||
-- player's own companion, hireling, or thrall - qualifies too, since
|
||||
-- discord scrambles a mind's sense of friend and foe entirely. Ties break
|
||||
-- toward the lower entity id, matching Game:nearestAlly's own tie rule.
|
||||
function Infighting.target(game, monster)
|
||||
local discorded = Status.has(monster, Status.DISCORD)
|
||||
local best, bestDist = nil, math.huge
|
||||
for _, other in ipairs(game.level.entities) do
|
||||
if other ~= monster and other:isAlive() then
|
||||
local qualifies = discorded or Factions.areRivals(monster.faction, other.faction)
|
||||
if qualifies and canSee(game, monster, other) then
|
||||
local dist = math.max(math.abs(other.x - monster.x), math.abs(other.y - monster.y))
|
||||
if dist < bestDist or (dist == bestDist and other.id < best.id) then
|
||||
best, bestDist = other, dist
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Resolves one monster-vs-monster hit through the same damage formula any
|
||||
-- other attack uses. A defender that falls is swept immediately: a
|
||||
-- companion/hireling/thrall dies exactly as game.lua already reports that,
|
||||
-- while a plain hostile monster just drops - Corpses.spawn still leaves
|
||||
-- remains behind, but no awardKill means no xp/essence/faction/compendium
|
||||
-- credit for a kill the player never made.
|
||||
function Infighting.strike(game, attacker, defender)
|
||||
local outcome = Combat.attack(attacker, defender)
|
||||
game.log:add(string.format("%s hits %s for %d", attacker.name, defender.name, outcome.damage))
|
||||
game:applyEliteOnHit(attacker, outcome.damage, defender)
|
||||
if attacker.biteDisease and defender:isAlive() and game.rng:chance(1, 4) then
|
||||
if Disease.infect(defender, attacker.biteDisease) then
|
||||
game.log:add(string.format("%s's bite leaves %s feeling ill", attacker.name, defender.name))
|
||||
end
|
||||
end
|
||||
game:emitNoise(attacker.x, attacker.y, Noise.MELEE)
|
||||
if not defender:isAlive() then
|
||||
if isAlly(defender) then
|
||||
game:onActorDeath(defender)
|
||||
else
|
||||
game.log:add(defender.name .. " dies")
|
||||
Corpses.spawn(game.level, defender)
|
||||
end
|
||||
end
|
||||
return outcome
|
||||
end
|
||||
|
||||
-- Entry point mirroring Abilities.tryAct's contract: returns true when
|
||||
-- infighting consumed the monster's turn (attacked an adjacent target or
|
||||
-- stepped toward a farther one), false to let the caller fall through to
|
||||
-- Ai.act.
|
||||
function Infighting.tryAct(game, monster)
|
||||
local target = Infighting.target(game, monster)
|
||||
if not target then return false end
|
||||
local dist = math.max(math.abs(target.x - monster.x), math.abs(target.y - monster.y))
|
||||
if dist <= 1 then
|
||||
Infighting.strike(game, monster, target)
|
||||
else
|
||||
game:stepMonsterToward(monster, target.x, target.y)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return Infighting
|
||||
Reference in new issue
Block a user