illarion-content/magic/arcane/movement.lua
2026-03-01 21:17:01 +01:00

380 lines
12 KiB
Lua

--[[
Illarion Server
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
]]
local runes = require("magic.arcane.runes")
local magicDamage = require("magic.arcane.magicDamage")
local effectScaling = require("magic.arcane.effectScaling")
local common = require("base.common")
local M = {}
local townEntrances = {
{lowerX = 116, upperX = 119, lowerY = 641, upperY = 641, z = 0},
{lowerX = 417, upperX = 417, lowerY = 249, upperY = 250, z = 0},
{lowerX = 385, upperX = 385, lowerY = 331, upperY = 331, z = 0},
{lowerX = 866, upperX = 866, lowerY = 814, upperY = 818, z = 0}
}
local texts = {
flying = {english = "A blast of air magic sends you flying.", german = "Ein Stoß von Luftmagie lässt dich fliegen."},
turn = {english = "A sudden burst of wind turns you around.", german = "Ein plötzlicher Windstoß wirbelt dich herum."}
}
local function checkIfItemMovable(item)
local id = item.id
local wear = item.wear
local weight = world:getItemStatsFromId(id).Weight
local bags = {97, 799}
for _, bag in pairs(bags) do
if id == bag then
return false
end
end
if weight >= 30000 or wear == 255 then
return false
end
return true
end
local function getListOfTownEntrancePositions()
local retList = {}
for _, entrance in pairs(townEntrances) do
for x = entrance.lowerX, entrance.upperX do
for y = entrance.lowerY, entrance.upperY do
local z = entrance.z
table.insert(retList, {pos = position(x, y, z)})
end
end
end
return retList
end
local function checkForCollisionAndFreeSpace(lx, ly, tx, ty, tz)
local entrancePositions = getListOfTownEntrancePositions()
local positionToCheck
local lastClearedPosition = position(lx, ly, tz)
local x
local y
if ty > ly then
y = ly+1
elseif ty < ly then
y = ly-1
else
y = ly
end
if tx > lx then
x = lx+1
elseif tx < lx then
x = lx-1
else
x = lx
end
positionToCheck = position(x, y, tz)
local field = world:getField(positionToCheck)
for _, entrance in pairs(entrancePositions) do
if entrance.pos == positionToCheck then
return true, lastClearedPosition
end
end
if not field then
return true, lastClearedPosition
elseif not field:isPassable() then
return true, lastClearedPosition
elseif world:isCharacterOnField(positionToCheck) then
return true, lastClearedPosition
end
if x == tx and y == ty then
return true, positionToCheck
end
return false, positionToCheck
end
local function getRangeOfMovement(user, spell, target, character, ORL, targetAmount, currentTarget)
local range = 1
local scaling = effectScaling.getEffectScaling(user, target, spell)
local QWAN = runes.checkSpellForRuneByName("QWAN", spell)
local KEL = runes.checkSpellForRuneByName("KEL", spell)
local TAUR = runes.checkSpellForRuneByName("TAUR", spell)
local URA = runes.checkSpellForRuneByName("URA", spell)
local YEG = runes.checkSpellForRuneByName("YEG", spell)
local JUS = runes.checkSpellForRuneByName("JUS", spell)
local rune
if TAUR then
rune = "TAUR"
elseif URA then
rune = "URA"
elseif YEG then
rune = "YEG"
end
local raceBonus
if rune and character then
raceBonus = magicDamage.checkIfRaceBonus(target, rune)
end
if raceBonus and JUS then
range = range + 1
end
if QWAN then
range = range + 2
end
if KEL then
if not M.KelVars.applied then
local rand = math.random(tonumber(currentTarget), tonumber(targetAmount))
if rand == tonumber(currentTarget) then
M.KelVars.applied = true
range = range + 1
end
end
end
if ORL and JUS then
range = range/2
end
range = range*scaling
if range < 1 then
range = 1
end
return range
end
local function checkIfWindSpell(spell)
if not runes.checkSpellForRuneByName("JUS", spell) then
return false
end
return true
end
local function getDirection(user, target, reverse)
local userX = user.pos.x
local userY = user.pos.y
local targetX = target.x
local targetY = target.y
if reverse then
userX = target.x
userY = target.y
targetX = user.pos.x
targetY = user.pos.y
end
local direction
if userX > targetX then
direction = "west"
if userY > targetY then
direction = "northwest"
elseif userY < targetY then
direction = "southwest"
end
elseif userX < targetX then
direction = "east"
if userY > targetY then
direction = "northeast"
elseif userY < targetY then
direction = "southeast"
end
elseif userY > targetY then
direction = "north"
else
direction = "south"
end
return direction
end
local directionCorrection = {}
table.insert(directionCorrection, {away = "west", toward = "east", X = 1, Y = 0})
table.insert(directionCorrection, {away = "east", toward = "west", X = -1, Y = 0})
table.insert(directionCorrection, {away = "north", toward = "south", X = 0, Y = 1})
table.insert(directionCorrection, {away = "south", toward = "north", X = 0, Y = -1})
table.insert(directionCorrection, {away = "southwest", toward = "northeast", X = 1, Y = -1})
table.insert(directionCorrection, {away = "southeast", toward = "northwest", X = -1, Y = -1})
table.insert(directionCorrection, {away = "northwest", toward = "southeast", X = 1, Y = 1})
table.insert(directionCorrection, {away = "northeast", toward = "southwest", X = -1, Y = 1})
local function moveTargets(user, target, spell, ORL, items, targetAmount, currentTarget)
local FHEN = runes.checkSpellForRuneByName("FHEN", spell)
local FHAN = runes.checkSpellForRuneByName("FHAN", spell)
local SAV = runes.checkSpellForRuneByName("SAV", spell)
local PHERC = runes.checkSpellForRuneByName("PHERC", spell)
local characters = not items
if not FHAN and not FHEN and not (SAV and not PHERC) then
return
end
if items then
if target and not checkIfItemMovable(target) then
return
end
end
local myTarget
if target then
myTarget = target.pos
else
myTarget = common.GetFrontPosition(user)
end
local direction = getDirection(user, myTarget)
local directionReverse = getDirection(user, myTarget, SAV)
local range = getRangeOfMovement(user, spell, target, characters, ORL, targetAmount, currentTarget)
local Z = myTarget.z
local landingX
local landingY
local directionToMove
local informEN = texts.flying.english
local informDE = texts.flying.german
local targetPosition
local landingXReverse
local landingYReverse
local targetPositionReverse
if FHAN and not FHEN then
directionToMove = "toward"
end
if FHEN and not FHAN then
directionToMove = "away"
end
for _, directions in pairs(directionCorrection) do
if directions[directionToMove] == direction then
landingX = myTarget.x + (directions.X*range)
landingY = myTarget.y + (directions.Y*range)
targetPosition = position(landingX, landingY, Z)
end
if directions.toward == directionReverse then
landingXReverse = user.pos.x + (directions.X*range)
landingYReverse = user.pos.y + (directions.Y*range)
targetPositionReverse = position(landingXReverse, landingYReverse, Z)
end
end
local lastX = myTarget.x
local lastY = myTarget.y
local lastXReverse = user.pos.x
local lastYReverse = user.pos.y
if FHAN or FHEN then
for i = 1, range+1 do
local positionCleared, thePosition = checkForCollisionAndFreeSpace(lastX, lastY, landingX, landingY, Z)
if not positionCleared then
lastX = thePosition.x
lastY = thePosition.y
else
targetPosition = thePosition
end
end
end
if SAV then
for i = 1, range+1 do
local positionCleared, thePosition = checkForCollisionAndFreeSpace(lastXReverse, lastYReverse, landingXReverse, landingYReverse, Z)
if not positionCleared then
lastXReverse = thePosition.x
lastYReverse = thePosition.y
else
targetPositionReverse = thePosition
end
end
end
if items then
if FHAN or FHEN then
local field = world:getField(myTarget)
local foundItems = field:countItems()
local targetItem = field:getStackItem(foundItems - 1)
world:erase(targetItem, targetItem.number)
world:createItemFromItem(targetItem, targetPosition, true)
end
elseif characters then
if FHAN or FHEN then
local targetedChar = world:getCharacterOnField(myTarget)
targetedChar:forceWarp(targetPosition)
targetedChar:inform(informDE, informEN)
end
end
if SAV then
user:forceWarp(targetPositionReverse)
user:inform(informDE, informEN)
end
end
local turningTargetsDirections = {
{direction = "west", to = 6},
{direction = "east", to = 2},
{direction = "north", to = 0},
{direction = "south", to = 4},
{direction = "southwest", to = 5},
{direction = "southeast", to = 3},
{direction = "northwest", to = 7},
{direction = "northeast", to = 1},
}
local function turnTarget(user, target, spell)
local LUK = runes.checkSpellForRuneByName("LUK", spell)
local TAH = runes.checkSpellForRuneByName("TAH", spell)
local direction = getDirection(user, target.pos)
if LUK and TAH then
return
end
if TAH then
target:turn(user.pos)
end
if LUK then
for _, turning in pairs(turningTargetsDirections) do
if direction == turning.direction then
target:turn(turning.to)
end
end
end
if LUK or TAH then
target:inform(texts.turn.german, texts.turn.english)
end
end
function M.applyMovementSpells(user, targets, spell, ORL)
if not checkIfWindSpell(spell) then
return
end
M.KelVars = {applied = false}
local targetAmount = #targets.targets + #targets.items
local currentTarget = 0
local ANTH = runes.checkSpellForRuneByName("ANTH", spell)
local SAV = runes.checkSpellForRuneByName("SAV", spell)
for _, target in pairs(targets.targets) do
currentTarget = currentTarget + 1
moveTargets(user, target, spell, ORL, false, targetAmount, currentTarget)
SAV = false
turnTarget(user, target, spell)
end
for _, item in pairs(targets.items) do
currentTarget = currentTarget + 1
if ANTH then
moveTargets(user, item, spell, ORL, true, targetAmount, currentTarget)
SAV = false
end
end
if SAV then -- if there has been no target to activate SAV but SAV exists
moveTargets(user, false, spell, ORL, false, 1, 1)
end
end
return M