feat: add reachableWithin
This commit is contained in:
1 parent
a9bc66b73d
commit
73bc8c133b
1 file changed
+158
+158
@@ -0,0 +1,158 @@
|
||||
-- Mana-fuelled spellbook. Spells are learned permanently from tomes (see
|
||||
-- Items.TOME) and cast by spending mana from a pool that regenerates each
|
||||
-- player turn. Targeted spells additionally need line of sight and range,
|
||||
-- checked here before any mana is spent so a bad cast is free. Damage and
|
||||
-- teleport spells reach into the game the same way potion/scroll effects do.
|
||||
|
||||
local Los = require("los")
|
||||
local Status = require("status")
|
||||
local Skills = require("skills")
|
||||
local Resistances = require("resistances")
|
||||
local Conductivity = require("conductivity")
|
||||
local Polymorph = require("polymorph")
|
||||
local Disease = require("disease")
|
||||
|
||||
local Spells = {}
|
||||
|
||||
local DIRS4 = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
|
||||
|
||||
-- Every walkable, unoccupied cell within `radius` orthogonal steps of the
|
||||
-- caster, excluding the caster's own cell. Used by blink so a short-range
|
||||
-- teleport only ever lands somewhere the caster could in principle walk to.
|
||||
local function reachableWithin(game, caster, radius)
|
||||
local map = game.level.map
|
||||
local start = map:index(caster.x, caster.y)
|
||||
local dist = { [start] = 0 }
|
||||
local queue = { { caster.x, caster.y, start } }
|
||||
local head = 1
|
||||
local cells = {}
|
||||
while head <= #queue do
|
||||
local cur = queue[head]
|
||||
head = head + 1
|
||||
local cx, cy, idx = cur[1], cur[2], cur[3]
|
||||
local d = dist[idx]
|
||||
if idx ~= start then cells[#cells + 1] = { x = cx, y = cy } end
|
||||
if d < radius then
|
||||
for _, dir in ipairs(DIRS4) do
|
||||
local nx, ny = cx + dir[1], cy + dir[2]
|
||||
if map:inBounds(nx, ny) and map:isWalkable(nx, ny) then
|
||||
local nIdx = map:index(nx, ny)
|
||||
if not dist[nIdx] then
|
||||
dist[nIdx] = d + 1
|
||||
queue[#queue + 1] = { nx, ny, nIdx }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return cells
|
||||
end
|
||||
|
||||
Spells.catalog = {
|
||||
missile = {
|
||||
name = "magic missile", cost = 3, targeted = true, range = 6,
|
||||
cast = function(game, caster, target)
|
||||
local raw = math.floor((6 + (caster.level or 1)) * Skills.spellDamageMultiplier(caster))
|
||||
local dmg = Resistances.apply(target, raw, Resistances.ARCANE)
|
||||
target.hp = target.hp - dmg
|
||||
if dmg <= 0 then
|
||||
game.log:add(string.format("a bolt of force washes over %s harmlessly", target.name))
|
||||
else
|
||||
game.log:add(string.format("a bolt of force hits %s for %d", target.name, dmg))
|
||||
end
|
||||
if not target:isAlive() then game:awardKill(target) end
|
||||
return {}
|
||||
end,
|
||||
},
|
||||
heal = {
|
||||
name = "minor heal", cost = 5, targeted = false,
|
||||
cast = function(_, caster)
|
||||
local amount = 10 + (caster.level or 1) * 2
|
||||
local before = caster.hp
|
||||
caster.hp = math.min(caster.maxHp, caster.hp + amount)
|
||||
return { string.format("you mend for %d", caster.hp - before) }
|
||||
end,
|
||||
},
|
||||
-- Mirrors effects.lua's "cure" potion handler (both lift disease and every
|
||||
-- Status.HOSTILE debuff) but reachable without needing the item on hand -
|
||||
-- until now the only way to shed poison/burning/stun/slow/weaken/discord/
|
||||
-- confuse/blind was to survive until they expired on their own.
|
||||
cleanse = {
|
||||
name = "cleansing ward", cost = 5, targeted = false,
|
||||
cast = function(_, caster)
|
||||
local diseaseCured = Disease.cure(caster)
|
||||
local lifted = Status.cureHostile(caster)
|
||||
if diseaseCured and #lifted > 0 then
|
||||
return { string.format("your affliction lifts, and the %s fades", table.concat(lifted, ", ")) }
|
||||
elseif diseaseCured then
|
||||
return { "your affliction lifts" }
|
||||
elseif #lifted > 0 then
|
||||
return { string.format("the %s fades", table.concat(lifted, ", ")) }
|
||||
end
|
||||
return { "you feel a faint tingle, but had nothing to cleanse" }
|
||||
end,
|
||||
},
|
||||
shield = {
|
||||
name = "arcane shield", cost = 4, targeted = false,
|
||||
cast = function(_, caster)
|
||||
Status.apply(caster, Status.SHIELD, 15, 1)
|
||||
return { "a shimmering ward surrounds you" }
|
||||
end,
|
||||
},
|
||||
shock = {
|
||||
name = "shock bolt", cost = 4, targeted = true, range = 5,
|
||||
cast = function(game, caster, target)
|
||||
local raw = math.floor((5 + (caster.level or 1)) * Skills.spellDamageMultiplier(caster))
|
||||
local dmg = Conductivity.applyLightning(target, raw)
|
||||
target.hp = target.hp - dmg
|
||||
if dmg <= 0 then
|
||||
game.log:add(string.format("a crackle of lightning washes over %s harmlessly", target.name))
|
||||
elseif Conductivity.isWet(target) then
|
||||
game.log:add(string.format("lightning arcs through the water clinging to %s for %d", target.name, dmg))
|
||||
else
|
||||
game.log:add(string.format("a bolt of lightning strikes %s for %d", target.name, dmg))
|
||||
end
|
||||
if not target:isAlive() then game:awardKill(target) end
|
||||
return {}
|
||||
end,
|
||||
},
|
||||
-- Fractures the target's mind for a few turns: while it lasts, the
|
||||
-- target attacks whatever creature stands nearest it - ally or foe,
|
||||
-- including the caster's own companion/hireling/thrall if one strays
|
||||
-- close enough - instead of chasing the caster (see infighting.lua). No
|
||||
-- direct damage of its own; the value is entirely in redirecting a
|
||||
-- threat, or turning a cluster of monsters against itself.
|
||||
discord = {
|
||||
name = "discord", cost = 5, targeted = true, range = 6,
|
||||
cast = function(_, _, target)
|
||||
Status.apply(target, Status.DISCORD, 8, 1)
|
||||
return { string.format("%s's mind fractures with discord", target.name) }
|
||||
end,
|
||||
},
|
||||
-- Scrambles the target's own sense of direction for a few turns (see
|
||||
-- status.lua's Status.CONFUSE, and its mirror in Game:movePlayer/Ai.act) -
|
||||
-- a lucky roll can send a dangerous chaser stumbling into a wall, a lava
|
||||
-- pool, or one of its own allies instead of at the caster.
|
||||
confuse = {
|
||||
name = "confusing hex", cost = 4, targeted = true, range = 5,
|
||||
cast = function(_, _, target)
|
||||
Status.apply(target, Status.CONFUSE, 6, 1)
|
||||
return { string.format("%s's senses reel with confusion", target.name) }
|
||||
end,
|
||||
},
|
||||
-- Snuffs the target's sight down to melee range for a few turns (see
|
||||
-- status.lua's Status.BLIND, folded into Game:effectiveRadius/canSeePlayer)
|
||||
-- - unlike confuse, which garbles the target's own movement, a blinded
|
||||
-- monster keeps walking straight at its last known heading and simply
|
||||
-- stops noticing anything past arm's reach, useful against archers and
|
||||
-- casters whose whole threat depends on seeing the caster from a distance.
|
||||
blind = {
|
||||
name = "veil of shadow", cost = 4, targeted = true, range = 5,
|
||||
cast = function(_, _, target)
|
||||
Status.apply(target, Status.BLIND, 6, 1)
|
||||
return { string.format("a veil of shadow falls over %s's eyes", target.name) }
|
||||
end,
|
||||
},
|
||||
-- Saps the target's fighting strength for a few turns (see status.lua's
|
||||
-- Status.WEAKEN). Until now WEAKEN only ever flowed toward the player - a
|
||||
-- shaman's ranged curse, a cursed shrine's backfired prayer - with no way
|
||||
Reference in new issue
Block a user