feat: extend game with Game:monsterDamageMultiplier
This commit is contained in:
1 parent
2d1cf09afa
commit
d04add4a7b
1 file changed
+148
-6
+148
-6
@@ -241,9 +241,34 @@ function Game:currentEntities()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Game:effectiveRadius()
|
function Game:effectiveRadius()
|
||||||
if not self.level.dark then return FOV_RADIUS end
|
local radius = FOV_RADIUS
|
||||||
if self.player.light.fuel > 0 then return self.player.light.radius end
|
if self.level.dark then
|
||||||
return 2
|
radius = self.player.light.fuel > 0 and self.player.light.radius or 2
|
||||||
|
end
|
||||||
|
radius = radius - Atmosphere.sightPenalty(self.level.weather) - Events.sightPenalty(self.level.event)
|
||||||
|
- Ascension.sightPenalty(self.ascension)
|
||||||
|
if radius < 1 then radius = 1 end
|
||||||
|
-- Blindness overrides every other light/weather/mutator term rather than
|
||||||
|
-- stacking with them: a blinded player run through with a full lantern in
|
||||||
|
-- clear weather still can't see past their own hand.
|
||||||
|
if Status.has(self.player, Status.BLIND) then radius = 1 end
|
||||||
|
return radius
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Folds events.lua's level-scoped monster damage swing together with
|
||||||
|
-- whatever ascension.lua mutators the player opted into for this run, read
|
||||||
|
-- live at the point a monster actually lands a hit (Game:monsterMelee,
|
||||||
|
-- Game:monsterShoot, Game:triggerMimic and Game:triggerGrave's ambush
|
||||||
|
-- swings) rather than baked into any monster's own stats.
|
||||||
|
function Game:monsterDamageMultiplier()
|
||||||
|
return Events.monsterDamageMultiplier(self.level.event) * Ascension.damageMultiplier(self.ascension)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Folds events.lua's rich-veins swing together with ascension.lua's own
|
||||||
|
-- essence mutators, read live wherever essence is actually credited
|
||||||
|
-- (Game:awardKill, Nodes.harvest).
|
||||||
|
function Game:essenceMultiplier()
|
||||||
|
return Events.essenceMultiplier(self.level.event) * Ascension.essenceMultiplier(self.ascension)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Game:computeVisible()
|
function Game:computeVisible()
|
||||||
@@ -252,6 +277,19 @@ function Game:computeVisible()
|
|||||||
for idx in pairs(self.visible) do
|
for idx in pairs(self.visible) do
|
||||||
self.level:markExplored(idx)
|
self.level:markExplored(idx)
|
||||||
end
|
end
|
||||||
|
self:noteSightings()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Records every currently-visible living monster's species into the
|
||||||
|
-- compendium (see compendium.lua) - silent bookkeeping, never touches the
|
||||||
|
-- message log, so it cannot perturb anything that reads recent log lines.
|
||||||
|
function Game:noteSightings()
|
||||||
|
local map = self.level.map
|
||||||
|
for _, entity in ipairs(self.level.entities) do
|
||||||
|
if entity.kind == "monster" and entity:isAlive() and self.visible[map:index(entity.x, entity.y)] then
|
||||||
|
Compendium.noteSighting(self.compendium, entity.species)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Game:cleanupDead()
|
function Game:cleanupDead()
|
||||||
@@ -265,6 +303,13 @@ function Game:hasKey()
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Game:hasAmulet()
|
||||||
|
for _, item in ipairs(self.player.inventory) do
|
||||||
|
if item.kind == Items.AMULET then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
function Game:randomFloorCell()
|
function Game:randomFloorCell()
|
||||||
local map = self.level.map
|
local map = self.level.map
|
||||||
local cells = {}
|
local cells = {}
|
||||||
@@ -294,9 +339,39 @@ function Game:playerAdjacent(monster)
|
|||||||
return math.abs(monster.x - self.player.x) + math.abs(monster.y - self.player.y) == 1
|
return math.abs(monster.x - self.player.x) + math.abs(monster.y - self.player.y) == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The player-aligned companion, hireling, or thrall standing right next to
|
||||||
|
-- `monster` (Manhattan-1, the same cardinal-only adjacency Game:playerAdjacent
|
||||||
|
-- itself uses), or nil if none is that close. A hostile monster whose only
|
||||||
|
-- route toward the player runs through a one-wide corridor an ally is
|
||||||
|
-- standing in cannot path around it (Game:stepMonsterToward's own
|
||||||
|
-- blockedForMonster treats any occupied non-goal cell as a wall), so without
|
||||||
|
-- this check every ai.lua pursuit behaviour just holds position forever once
|
||||||
|
-- it reaches that ally's doorstep - the ally becomes an unkillable, un-
|
||||||
|
-- fightable roadblock rather than the combatant Game:onActorDeath and the
|
||||||
|
-- companion/mercenary/thrall noise-emission work already treat it as. Ties
|
||||||
|
-- break toward the lower entity id, matching Game:nearestAlly's own rule.
|
||||||
|
function Game:adjacentAlly(monster)
|
||||||
|
local best
|
||||||
|
for _, other in ipairs(self.level.entities) do
|
||||||
|
if other ~= monster and other:isAlive()
|
||||||
|
and (other.kind == "companion" or other.kind == "hireling" or other.kind == "thrall")
|
||||||
|
and math.abs(other.x - monster.x) + math.abs(other.y - monster.y) == 1
|
||||||
|
and (not best or other.id < best.id) then
|
||||||
|
best = other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
function Game:canSeePlayer(monster, range)
|
function Game:canSeePlayer(monster, range)
|
||||||
|
local penalty = Skills.detectionPenalty(self.player, self.level.dark)
|
||||||
|
local effectiveRange = math.max(range - penalty, 0)
|
||||||
|
-- A blinded monster can still feel the player out at melee range but loses
|
||||||
|
-- every longer-ranged aggro/pack/flee/ranged check in ai.lua - the same
|
||||||
|
-- clamp Game:effectiveRadius applies to a blinded player's own sight.
|
||||||
|
if Status.has(monster, Status.BLIND) then effectiveRange = math.min(effectiveRange, 1) end
|
||||||
local cheb = math.max(math.abs(monster.x - self.player.x), math.abs(monster.y - self.player.y))
|
local cheb = math.max(math.abs(monster.x - self.player.x), math.abs(monster.y - self.player.y))
|
||||||
if cheb > range then return false end
|
if cheb > effectiveRange then return false end
|
||||||
return Los.clear(self.level.map, monster.x, monster.y, self.player.x, self.player.y)
|
return Los.clear(self.level.map, monster.x, monster.y, self.player.x, self.player.y)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -305,20 +380,87 @@ function Game:hasClearShot(monster)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Game:monsterMelee(monster)
|
function Game:monsterMelee(monster)
|
||||||
local outcome = Combat.attack(monster, self.player)
|
if Skills.evades(self, self.player) then
|
||||||
|
self.log:add(string.format("you evade %s's attack", monster.name))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local outcome = Combat.attack(monster, self.player, self:monsterDamageMultiplier())
|
||||||
|
Momentum.onDamaged(self.player)
|
||||||
self.log:add(string.format("%s hits you for %d", monster.name, outcome.damage))
|
self.log:add(string.format("%s hits you for %d", monster.name, outcome.damage))
|
||||||
|
local broke = Durability.wearArmor(self.player)
|
||||||
|
if broke then self.log:add(broke) end
|
||||||
|
self:applyEliteOnHit(monster, outcome.damage, self.player)
|
||||||
|
local thorns = Artifacts.thornsAmount(self.player, outcome.damage)
|
||||||
|
if thorns > 0 then
|
||||||
|
-- Aegis of Embers "sears" its attacker by name and by log line, so this
|
||||||
|
-- reflect is fire damage, not a bare stat subtraction - a fire-immune
|
||||||
|
-- dragon (bestiary-real, reachable from depth 6) shrugs it off entirely,
|
||||||
|
-- exactly like every other fire source in the engine already does.
|
||||||
|
local dealt = Resistances.apply(monster, thorns, Resistances.FIRE)
|
||||||
|
if dealt > 0 then
|
||||||
|
monster.hp = monster.hp - dealt
|
||||||
|
self.log:add(string.format("your armor sears %s for %d", monster.name, dealt))
|
||||||
|
if not monster:isAlive() then self:awardKill(monster) end
|
||||||
|
else
|
||||||
|
self.log:add(string.format("your armor's embers wash harmlessly over %s", monster.name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if monster.biteDisease and self.player:isAlive() and self.rng:chance(1, 4) then
|
||||||
|
if Disease.infect(self.player, monster.biteDisease) then
|
||||||
|
self.log:add(string.format("%s's bite leaves you feeling ill", monster.name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:emitNoise(monster.x, monster.y, Noise.MELEE)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A vampiric elite heals a portion of the damage it just dealt; a chilling
|
||||||
|
-- one leaves its status rider on the victim. Shared by melee and a ranged
|
||||||
|
-- shot so either delivery route triggers the same elite hooks - and, since
|
||||||
|
-- Infighting.strike resolves an elite fighting a rival monster or the
|
||||||
|
-- player's own companion/hireling/thrall through this same method, the log
|
||||||
|
-- line names the actual victim instead of always assuming it is the player.
|
||||||
|
function Game:applyEliteOnHit(attacker, dealt, victim)
|
||||||
|
local lifesteal = Elites.lifestealFraction(attacker)
|
||||||
|
if lifesteal > 0 then
|
||||||
|
local healed = math.floor(dealt * lifesteal)
|
||||||
|
if healed > 0 and attacker:isAlive() then
|
||||||
|
attacker.hp = math.min(attacker.maxHp, attacker.hp + healed)
|
||||||
|
local from = victim == self.player and "you" or victim.name
|
||||||
|
self.log:add(string.format("%s drains %d hp from %s", attacker.name, healed, from))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local rider = Elites.onHitRider(attacker)
|
||||||
|
if rider and victim:isAlive() then
|
||||||
|
Status.apply(victim, rider.status, rider.duration, rider.potency)
|
||||||
|
local who = victim == self.player and "you" or victim.name
|
||||||
|
self.log:add(string.format("%s's touch leaves %s %s", attacker.name, who, rider.status))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Game:monsterShoot(monster)
|
function Game:monsterShoot(monster)
|
||||||
local dealt = Combat.damage(monster, self.player)
|
local damageType = monster.breath == "fire" and Resistances.FIRE or Resistances.PHYSICAL
|
||||||
|
local dealt = Combat.damage(monster, self.player, self:monsterDamageMultiplier(), damageType)
|
||||||
self.player.hp = self.player.hp - dealt
|
self.player.hp = self.player.hp - dealt
|
||||||
|
Momentum.onDamaged(self.player)
|
||||||
self.log:add(string.format("%s shoots you for %d", monster.name, dealt))
|
self.log:add(string.format("%s shoots you for %d", monster.name, dealt))
|
||||||
|
local broke = Durability.wearArmor(self.player)
|
||||||
|
if broke then self.log:add(broke) end
|
||||||
|
self:applyEliteOnHit(monster, dealt, self.player)
|
||||||
|
if monster.breath == "fire" then
|
||||||
|
if Resistances.tierOf(self.player, Resistances.FIRE) == Resistances.IMMUNE then
|
||||||
|
self.log:add("the flames wash over you harmlessly")
|
||||||
|
else
|
||||||
|
Status.apply(self.player, Status.BURNING, 3, 2)
|
||||||
|
self.log:add("the flames catch you alight")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function blockedForMonster(game, self, goalX, goalY)
|
local function blockedForMonster(game, self, goalX, goalY)
|
||||||
return function(x, y)
|
return function(x, y)
|
||||||
if x == goalX and y == goalY then return false end
|
if x == goalX and y == goalY then return false end
|
||||||
if not game.level.map:isWalkable(x, y) then return true end
|
if not game.level.map:isWalkable(x, y) then return true end
|
||||||
|
if game.level:blockAt(x, y) then return true end
|
||||||
if x == game.player.x and y == game.player.y then return true end
|
if x == game.player.x and y == game.player.y then return true end
|
||||||
local occupant = game.level:entityAt(x, y)
|
local occupant = game.level:entityAt(x, y)
|
||||||
return occupant ~= nil and occupant ~= self
|
return occupant ~= nil and occupant ~= self
|
||||||
|
|||||||
Reference in new issue
Block a user