143 lines
4.8 KiB
Lua
143 lines
4.8 KiB
Lua
-- Monster behaviours, one per ai tag. Each takes (game, monster) and fully
|
|
-- resolves that monster's turn through game helpers, so all state mutation and
|
|
-- rng use stays centralised and deterministic.
|
|
|
|
local Status = require("status")
|
|
local Infighting = require("infighting")
|
|
|
|
local Ai = {}
|
|
|
|
local AGGRO = { chase = 10, pack = 13, ranged = 12, flee = 8, erratic = 8, tunnel = 10 }
|
|
local DIRS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
|
|
|
|
-- A player-aligned ally standing adjacent to `monster` occupies its cell
|
|
-- exactly the way a wall would for Game:stepMonsterToward's own pathfinding
|
|
-- (blockedForMonster treats any occupied non-goal cell as impassable) - so a
|
|
-- one-wide corridor an ally is plugging leaves every pursuit behaviour below
|
|
-- with no route at all, and without this check they simply idle beside it
|
|
-- forever rather than fighting the living combatant in their way. Resolves
|
|
-- through Infighting.strike, the same generic monster-vs-any-creature combat
|
|
-- infighting.lua already uses for rivalries and discord. Returns true once
|
|
-- it has spent the monster's turn this way, so each caller can bail out at
|
|
-- the same priority Game:playerAdjacent already has over closing distance.
|
|
local function attackBlockingAlly(game, monster)
|
|
local ally = game:adjacentAlly(monster)
|
|
if not ally then return false end
|
|
Infighting.strike(game, monster, ally)
|
|
return true
|
|
end
|
|
|
|
local function chase(game, monster)
|
|
if game:playerAdjacent(monster) then
|
|
game:monsterMelee(monster)
|
|
return
|
|
end
|
|
if attackBlockingAlly(game, monster) then return end
|
|
if game:canSeePlayer(monster, AGGRO.chase) then
|
|
game:stepMonsterToward(monster, game.player.x, game.player.y)
|
|
end
|
|
end
|
|
|
|
local function pack(game, monster)
|
|
if game:playerAdjacent(monster) then
|
|
game:monsterMelee(monster)
|
|
return
|
|
end
|
|
if attackBlockingAlly(game, monster) then return end
|
|
if game:canSeePlayer(monster, AGGRO.pack) then
|
|
game:stepMonsterToward(monster, game.player.x, game.player.y)
|
|
return
|
|
end
|
|
local ally = game:nearestAlly(monster)
|
|
if ally then
|
|
game:stepMonsterToward(monster, ally.x, ally.y)
|
|
end
|
|
end
|
|
|
|
local function ranged(game, monster)
|
|
if game:canSeePlayer(monster, monster.range) and game:hasClearShot(monster) then
|
|
if game:playerAdjacent(monster) then
|
|
game:stepMonsterAway(monster, game.player.x, game.player.y)
|
|
else
|
|
game:monsterShoot(monster)
|
|
end
|
|
return
|
|
end
|
|
if attackBlockingAlly(game, monster) then return end
|
|
if game:canSeePlayer(monster, AGGRO.ranged) then
|
|
game:stepMonsterToward(monster, game.player.x, game.player.y)
|
|
end
|
|
end
|
|
|
|
local function flee(game, monster)
|
|
if game:canSeePlayer(monster, AGGRO.flee) then
|
|
game:stepMonsterAway(monster, game.player.x, game.player.y)
|
|
end
|
|
end
|
|
|
|
-- Shopkeepers idle unless game.lua has already flagged them hostile (a theft
|
|
-- from their stock), at which point they behave exactly like a chaser.
|
|
local function vend(game, monster)
|
|
if monster.hostile then
|
|
chase(game, monster)
|
|
end
|
|
end
|
|
|
|
local function erratic(game, monster)
|
|
if game:playerAdjacent(monster) then
|
|
game:monsterMelee(monster)
|
|
return
|
|
end
|
|
if game:canSeePlayer(monster, AGGRO.erratic) and game.rng:chance(1, 2) then
|
|
game:stepMonsterToward(monster, game.player.x, game.player.y)
|
|
else
|
|
local dir = DIRS[game.rng:range(1, 4)]
|
|
game:moveMonster(monster, dir[1], dir[2])
|
|
end
|
|
end
|
|
|
|
-- A burrower: melee once adjacent, chase like anything else once it can see
|
|
-- the player through open floor, otherwise claw straight through whatever
|
|
-- rock stands between them (see Game:monsterDig, digging.lua) rather than
|
|
-- idling the way a plain chaser out of sight would.
|
|
local function tunnel(game, monster)
|
|
if game:playerAdjacent(monster) then
|
|
game:monsterMelee(monster)
|
|
return
|
|
end
|
|
if attackBlockingAlly(game, monster) then return end
|
|
if game:canSeePlayer(monster, AGGRO.tunnel) then
|
|
game:stepMonsterToward(monster, game.player.x, game.player.y)
|
|
return
|
|
end
|
|
game:monsterDig(monster, game.player.x, game.player.y)
|
|
end
|
|
|
|
Ai.behaviours = {
|
|
chase = chase,
|
|
pack = pack,
|
|
ranged = ranged,
|
|
flee = flee,
|
|
erratic = erratic,
|
|
vend = vend,
|
|
tunnel = tunnel,
|
|
}
|
|
|
|
-- A confused monster gets the exact same chance each turn to have its own
|
|
-- chosen direction overridden that Game:movePlayer rolls for the player - see
|
|
-- status.lua's Status.CONFUSE_NUMERATOR/DENOMINATOR, shared by both sides so
|
|
-- neither one confuses more reliably than the other. On a lucky roll (or once
|
|
-- confusion wears off) `behaviour` below runs completely untouched.
|
|
function Ai.act(game, monster)
|
|
if Status.has(monster, Status.CONFUSE)
|
|
and game.rng:chance(Status.CONFUSE_NUMERATOR, Status.CONFUSE_DENOMINATOR) then
|
|
local dir = DIRS[game.rng:range(1, #DIRS)]
|
|
game:moveMonster(monster, dir[1], dir[2])
|
|
return
|
|
end
|
|
local behaviour = Ai.behaviours[monster.ai] or chase
|
|
behaviour(game, monster)
|
|
end
|
|
|
|
return Ai
|