illarion-content/magic/arcane/spatial.lua
2026-01-04 23:30:08 +01:00

976 lines
34 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 common = require("base.common")
local magic = require("base.magic")
local spiritlocation = require("magic.arcane.spirit.location")
local antiTroll = require("magic.base.antiTroll")
local shared = require("magic.arcane.enchanting.core.shared")
local tutorials = require("magic.tutorials")
local globalvar = require("base.globalvar")
local M = {}
local portalSpots = antiTroll.portals
local myTexts = {
portalBookNeeded = {
english = "You must have the portal book you wish to evaluate in one of your belt slots.",
german = "Du musst das Portalbuch, das du bewerten möchtest, in einem deiner Gürtelplätze haben."}, --Verification of chatGPT GERMAN TRANSLATION needed
distanceTexts = {
{value = 600, english = " very far ", german = "sehr fern "},
{value = 400, english = " far ", german = "fern "},
{value = 100, english = " somewhere ", german = "in der Nähe "},
{value = 50, english = " close ", german = "nahe "},
{value = 0, english = " very close ", german = "sehr nahe "},
},
binderTexts = {
english = "to the ",
german = "in Richtung "
},
alreadyAttuned = {
english = "You've already attuned your spatial magic to this location.",
german = "Du hast deine Raummagie bereits auf diesen Ort abgestimmt."
},
closePortal = {
english = "Close a portal",
german = "Schließe ein Portal."
},
doneAttuning = {
english = "You've attuned to the crossing mana lines in the area. You will now be able to remember the spatial coordinates to teleport, cast portals and even write portal books that lead to this area, should your expertise in spatial magic allow.",
german = "Du hast deine Raummagie auf die sich kreuzenden Manalinien dieses Gebiets abgestimmt. Du kannst dir nun die räumlichen Koordinaten merken um dich her zu teleportieren, Portale zu öffnen oder sogar Portalbücher schreiben die dich hier her führen. Natürlich nur wenn es deine Erfahrung in Raummagie erlaubt."
},
lackingSkill = {
english = "You need to attain a higher level of expertise in spatial magic to remember the spatial coordinates for this location.",
german = "Um dir die räumlichen Koordinaten für diesen Ort merken zu können, musst du deine Fähigkeiten in der Raummagie verbessern."
},
lackingAttribs = {
english = "You do not have the required magical prowess to attune to this particular spatial node.",
german = "Du besitzt nicht die nötige magische Begabung, um dich mit diesem speziellen räumlichen Knotenpunkt zu verbinden."
},
noNearby = {
english = "There are no crossing mana lines nearby for you to attune your spatial magic to. The nearest one is ",
german = "Hier befinden sich keine kreuzenden Manalinien um deine Raummagie abzustimmen. Die Näheste befindet sich "
},
red = {
english = "Portal colour set to red",
german = "Die Farbe deiner Portale ist nun rot."
},
blue = {
english = "Portal colour set to blue",
german = "Die Farbe deiner Portale ist nun blau."
},
name = {
english = "Spatial Magic",
german = "Raummagie"
},
selectColour = {
english = "Select which colour your portals should have.",
german = "Wähle welche Farbe deine Portale haben sollen."
},
colours = {
red = { english = "Red", german = "Rot"},
blue = { english = "Blue", german = "Blau"}
},
incantation = {
portal = "Locus Ianua", -- latin for "space door"
teleport = "Locus Itinerantur", --latin for "space travel"
attune = "Locus Memento", --latin, "space remember"
close = "Locus Claudere" -- "space shut"
},
teleportation = {
english = "Teleportation",
german = "Teleportation"
},
destination = {
english = "Select your destination",
german = "Wähle deinen Zielort"
},
castSelect = {
english = "What kind of spatial magic do you want to cast?",
german = "Welche Raummagie möchtest du sprechen?"
},
attuneTo = {
english = "Attune to location",
german = "Abstimmung mit einem Ort"
},
teleport = {
english = "Teleport",
german = "Teleport"
},
portal = {
english = "Create Portal",
german = "Erschaffe ein Portal"
},
portalColour = {
english = "Portal colour",
german = "Portalfarbe"
},
interruptedCast = {
english = "Your casting of spatial magic was interrupted.",
german = "Das wirken deiner Raummagie wurde unterbrochen."
},
cantFindMore = {
english = "You don't detect any more crossing mana lines to attune to. Perhaps you've found them all?",
german = "Du kannst keine sich kreuzenden Manalinien mehr ausmachen. Vielleicht hast du ja schon alle gefunden?"
},
differentElevation = {
english = "You can feel that there are crossed mana lines nearby that you can attune your spatial magic to, however not here. Perhaps you'd had better luck searching at a different elevation?",
german = "Du kannst spüren, dass du dich direkt in der Nähe einer sich kreuzenden Manalinie befindest, an der du deine Raummagie abstimmen könntest. Es scheint aber als würde sich dieser auf einer anderen Ebene befinden."
},
badTarget = {
english = "Something in the area disrupts your casting. Perhaps this isn't a good spot for spatial magic?",
german = "Etwas in der Nähe unterbericht deinen Zauber. Möglicherweise ist das kein guter Ort für Raummagie?"
},
locationBlocked = {
english = "There is not enough free space in front of you for a portal, try again somewhere there is more room.",
german = "Vor dir ist nicht genug Platz für ein Portal. Versuche es erneut an einem Ort, wo mehr Raum zur Verfügung steht."
},
showBookQuality = {
english = "Portal Book Evaluation",
german = "Portalbuchbewertung"
},
bookQuality = {
english = "Through your inspection you find that this portal book is of ",
german = "Durch deine Inspektion stellst du fest, dass dieses Portalbuch von der Qualität "
},
bookQualityAddendum = {
english = " quality. The better the quality, the faster it is to summon a portal through the usage of this book.",
german = " ist. Je besser die Qualität, desto schneller lässt sich ein Portal mithilfe dieses Buches beschwören."
}
}
M.spatialTexts = myTexts
local function findSpotNameBasedOnCoord(coordinates)
for _, spot in pairs(portalSpots) do
if spot.location == coordinates then
return spot.nameEn, spot.nameDe
end
end
return false
end
function M.attunedSpotsToQuestprogress(attunedSpots)
if attunedSpots < 2^31 then
return attunedSpots
else
return 2^31- 1 - attunedSpots
end
end
function M.QuestprogressToAttunedSpots(questprogress)
if tonumber(questprogress)<0 then
return 2^31-1-tonumber(questprogress)
else
return tonumber(questprogress)
end
end
local function userMeetsAttribReq(user, statReq)
local baseReq, sum = magic.hasMageAttributes(user)
if not statReq and baseReq then
return true
end
if sum >= statReq then
return true
end
return false
end
function M.spotAttuned(user, spotNumber)
local attunedSpots = M.QuestprogressToAttunedSpots(user:getQuestProgress(216))
local retVal = false
local offset = math.fmod(spotNumber-1,32)
if bit32.btest(bit32.lshift(1, offset), attunedSpots) then
retVal=true
end
if not userMeetsAttribReq(user, portalSpots[spotNumber].statReq) then --To prevent people temporarily changing attribs or using pots to learn this and then cast it with lower than attrib req, we check it here
return false
end
return retVal
end
function M.attuneSpot(user, spotNumber)
local offset= math.fmod(spotNumber-1,32);
local attunedSpots = M.QuestprogressToAttunedSpots(user:getQuestProgress(216))
user:setQuestProgress(216, M.attunedSpotsToQuestprogress(bit32.bor(2^offset, attunedSpots)))
end
local function getDistance(userXY, targetXY)
if userXY > targetXY then
return userXY-targetXY
elseif userXY < targetXY then
return targetXY-userXY
else
return 0
end
end
local function checkIfDifferentElevation(userPos, targetPos)
if userPos.z == targetPos.z then
return false
end
return true
end
local function getDirectionDistance(user)
local usersLevel = user:getSkill(Character.spatialMagic)
local distance
local targetPos
local distanceWithinLevelRange
local levelRangeTargetPos
for i = 1, #portalSpots do
if not M.spotAttuned(user, i) and userMeetsAttribReq(user, portalSpots[i].statReq) then
local location = portalSpots[i].location
if location.z ~= 0 and user.pos.z ~= location.z then
--Lead to the entrance instead of the underground coordinate since the coordinates above and below ground sadly do not match
location = portalSpots[i].entrance
end
local xDiff = getDistance(user.pos.x, location.x)
local yDiff = getDistance(user.pos.y, location.y)
local total = xDiff+yDiff
if not distance then
distance = total
targetPos = location
if portalSpots[i].level <= usersLevel then
distanceWithinLevelRange = total
levelRangeTargetPos = location
end
elseif total < distance then
distance = total
targetPos = location
if portalSpots[i].level <= usersLevel then
if not distanceWithinLevelRange then
distanceWithinLevelRange = total
levelRangeTargetPos = location
elseif total < distanceWithinLevelRange then
distanceWithinLevelRange = total
levelRangeTargetPos = location
end
end
end
end
end
-- the nearby portal spots check will prioritize ones within your level range to attune to
if distanceWithinLevelRange and levelRangeTargetPos then
distance = distanceWithinLevelRange
targetPos = levelRangeTargetPos
end
if not distance then --No unattuned spots left
user:inform(myTexts.cantFindMore.german, myTexts.cantFindMore.english)
return false
end
if distance == 0 then
if checkIfDifferentElevation(user.pos, targetPos) then
user:inform(myTexts.differentElevation.german, myTexts.differentElevation.english)
return false
end
end
local directionNameEn = spiritlocation.getDirectionRelativeToUser(user, targetPos)
local directionNameDe = spiritlocation.getGermanDirection(directionNameEn)
local binderTextEn = myTexts.binderTexts.english
local binderTextDe = myTexts.binderTexts.german
local distanceTextEn
local distanceTextDe
local distanceTexts = myTexts.distanceTexts
for _, distanceText in ipairs(distanceTexts) do
if distance > distanceText.value then
distanceTextEn = distanceText.english
distanceTextDe = distanceText.german
break
end
end
return common.GetNLS(user, distanceTextDe..binderTextDe..directionNameDe, distanceTextEn..binderTextEn..directionNameEn)
end
local startCycles = 3
function M.checkSpotEligiblity(user, actionState)
for i = 1, #portalSpots do
if user.pos.x >= portalSpots[i].location.x-5 and user.pos.x <= portalSpots[i].location.x+5 then
if user.pos.y >= portalSpots[i].location.y-5 and user.pos.y <= portalSpots[i].location.y+5 then
if user.pos.z == portalSpots[i].location.z then
local spatialMagicLevel = user:getSkill(Character.spatialMagic)
if portalSpots[i].nameEn == "Troll's Haven" then
if user:getQuestProgress(240) == 1 then
user:setQuestProgress(240, 2)
end
end
if M.spotAttuned(user, i) then
user:inform(myTexts.alreadyAttuned.german, myTexts.alreadyAttuned.english)
return
else
if spatialMagicLevel >= portalSpots[i].level and userMeetsAttribReq(user, portalSpots[i].statReq) then
M[user.name.."attunement"] = i
M[user.name.."cycles"] = startCycles
M.startCycle(user, actionState)
return
elseif not userMeetsAttribReq(user, portalSpots[i].statReq) then
user:inform(myTexts.lackingAttribs.german, myTexts.lackingAttribs.english)
else
user:inform(myTexts.lackingSkill.german, myTexts.lackingSkill.english)
return
end
end
end
end
end
end
if not getDirectionDistance(user) then
return
end
user:inform(myTexts.noNearby.german..getDirectionDistance(user), myTexts.noNearby.english..getDirectionDistance(user))
end
local function getListOfAttunedSpots(user, portal)
local spots = {}
for i = 1, #portalSpots do
if M.spotAttuned(user, i) then
spots[#spots+1] = portalSpots[i]
end
end
return spots
end
local minimumWear = 1
local maxWearIncrease = 5
local function getPortalWear(user)
local leadAttribNames = common.GetLeadAttributeName(Character.spatialMagic)
local leadAttribValue1 = user:increaseAttrib(leadAttribNames.first, 0) * 0.6
local leadAttribValue2 = user:increaseAttrib(leadAttribNames.second, 0) * 0.4
local statBonus = leadAttribValue1 + leadAttribValue2
local wandQualityBonus = magic.getQualityBonusWand(user) -- A perfect wand equals a bonus of 0.1
local wandGemBonus = magic.getGemBonusWand(user) -- Each tier of a magic set equals 0.06
local equipmentBonus = magic.getMagicBonus(user)
local totalBonus = statBonus + wandQualityBonus + wandGemBonus/100 + equipmentBonus
--[[
To reach the best wear duration, a sum of 1.2 is needed.
Example scenario to reach it:
Mage with 20 intelligence and willpower. Adds a bonus of 0.5.
Perfect wand. 0.5 + 0.1 = 0.6
Wear equipment suitable for magic (high level jewellery, clothing instead of armour), 0.6 + 0.1 = 0.7
Tier 5 gems. 0.7 + 0.30 = 1
Drink a +4 intelligence and willpower potion = 1.21
Eat a +2 intelligence and willpower meal = 1.31
The reason behind 1.2 is to make consumables and gems
useful for even a well statted mage, and not allow consumables
and gems to entirely replace stat requirements.
Above tier 5 gems it gets considerably harder to reach higher tiers.
This means if you want to replace consumables or have 6 lower intelligence and willpower
and still reach the maximum, you would need tier 8-9 gems.
]]
local increase = math.min((maxWearIncrease/120)*(totalBonus*100), maxWearIncrease)
local wear = minimumWear + increase
return math.ceil(wear)
end
local baseCastTime = 200 -- 20 seconds as a basis, the static teleporters use this
local maxReduction = 100 -- Mages cast teleport and portals at half the time of a teleporter, 10 seconds, and portal books of perfect quality only available from players can also reach this speed.
local function getCastTime(user, wand)
-- Since learning scales with cast time, it makes no sense to lower it based on any criteria. As such, it is a flat value.
return baseCastTime - maxReduction
end
function M.getBookCastDuration(portalBook)
if not portalBook then -- It's the teleporter
return baseCastTime
end
local quality = math.floor(portalBook.quality/100)+1
local reducedCastTime = baseCastTime - (maxReduction/10)*quality
return reducedCastTime
end
local function getBookQualityText(portalBook)
local quality = math.floor(portalBook.quality/100)
local germanText = shared.qualityTexts.german[quality]
local englishText = shared.qualityTexts.english[quality]
return germanText, englishText
end
local belt = {Character.belt_pos_1, Character.belt_pos_2, Character.belt_pos_3, Character.belt_pos_4, Character.belt_pos_5, Character.belt_pos_6}
function M.listBooksAtBelt(user)
local portalBookId = 1061
local retList = {}
for _, beltSlot in pairs(belt) do
local beltItem = user:getItemAt(beltSlot)
if portalBookId == beltItem.id and not common.IsNilOrEmpty(beltItem:getData("destinationCoordsZ")) then --it is a portal book
table.insert(retList, beltItem)
end
end
return retList
end
local function getDestinationText(book)
local destinationEn = book:getData("destinationEn")
local destinationDe = book:getData("destinationDe")
if common.IsNilOrEmpty(destinationDe) then
local Z = tonumber(book:getData("destinationCoordsZ"))
local X = tonumber(book:getData("destinationCoordsX"))
local Y = tonumber(book:getData("destinationCoordsY"))
local coordinates = false
if not common.IsNilOrEmpty(Z) and not common.IsNilOrEmpty(X) and not common.IsNilOrEmpty(Y) then
coordinates = position(X, Y, Z)
end
if coordinates then
destinationEn, destinationDe = findSpotNameBasedOnCoord(coordinates)
end
if common.IsNilOrEmpty(destinationDe) then
-- The coordinates do not match any mage attuned location
destinationEn = "Unknown"
destinationDe = "Unbekannt"
end
end
local prefixEn = "Destination: "
local prefixDe = "Ziel: "
return prefixDe..destinationDe.."\n\n", prefixEn..destinationEn.."\n\n"
end
function M.showBookQuality(user)
local booksAtBelt = M.listBooksAtBelt(user)
if #booksAtBelt <= 0 then
user:inform(myTexts.portalBookNeeded.german, myTexts.portalBookNeeded.english)
return
end
local callback = function(dialog)
if not dialog:getSuccess() then
return
end
local selected = dialog:getSelectedIndex() + 1
local germanTitle = myTexts.showBookQuality.german
local englishTitle = myTexts.showBookQuality.english
local germanText, englishText = getBookQualityText(booksAtBelt[selected])
local germanTextDestination, englishTextDestination = getDestinationText(booksAtBelt[selected])
local callback = function(MsgDialog)
end
local MsgDialog = MessageDialog(common.GetNLS(user, germanTitle, englishTitle), common.GetNLS(user, germanTextDestination..myTexts.bookQuality.german..germanText..myTexts.bookQualityAddendum.german, englishTextDestination..myTexts.bookQuality.english..englishText..myTexts.bookQualityAddendum.english), callback)
user:requestMessageDialog(MsgDialog)
end
local title = common.GetNLS(user,"Untersuchung von Portalbüchern", "Inspection of portal books")
local description = common.GetNLS(user, "Wähle das Portalbuch aus, das du untersuchen möchtest.", "Select the portal book you wish to inspect.")
local dialog = SelectionDialog(title, description, callback)
for _, book in pairs(booksAtBelt) do
local commonItem = world:getItemStatsFromId(book.id)
dialog:addOption(book.id, common.GetNLS(user, commonItem.German, commonItem.English))
end
dialog:setCloseOnMove()
user:requestSelectionDialog(dialog)
end
local function getManaCost(user)
local baseCost = 5000
local maxDecrease = 2500
local essence = user:increaseAttrib("essence", 0)
local essenceBonus = common.GetAttributeBonusHigh(essence) -- A essence of 20 equals a bonus of 0.5, 24 = 0.71, 26 = 0.81
local wandQualityBonus = magic.getQualityBonusWand(user) -- A perfect wand equals a bonus of 0.1
local wandGemBonus = magic.getGemBonusWand(user) -- Each tier of a magic set equals 0.06
local equipmentBonus = magic.getMagicBonus(user)
local totalBonus = essenceBonus + wandQualityBonus + wandGemBonus/100 + equipmentBonus
--[[
To reach the best mana cost, a sum of 1.2 is needed.
Example scenario to reach it:
Mage with 20 essence. Adds a bonus of 0.5.
Perfect wand. 0.5 + 0.1 = 0.6
Wear equipment suitable for magic (high level jewellery, clothing instead of armour), 0.6 + 0.1 = 0.7
Tier 5 gems. 0.7 + 0.30 = 1
Drink a +4 essence potion = 1.21
Eat a +2 essence meal = 1.31
The reason behind 1.2 is to make consumables and gems
useful for even a well statted mage, and not allow consumables
and gems to entirely replace stat requirements.
Above tier 5 gems it gets considerably harder to reach higher tiers.
This means if you want to replace consumables or have 6 lower essence
and still reach the maximum, you would need tier 8-9 gems.
]]
local decrease = math.min((maxDecrease/120)*(totalBonus*100), maxDecrease)
local reducedCost = baseCost - decrease
return reducedCost
end
local function checkIfEnoughMana(user)
local mana = getManaCost(user)
if magic.hasSufficientMana(user, mana) then
return true
end
return false
end
local function removeUsedMana(user)
local mana = getManaCost(user)
user:increaseAttrib("mana", -mana)
end
function M.selectPortalColor(user)
local callback = function (dialog)
if (not dialog:getSuccess()) then
return
end
local index = dialog:getSelectedIndex() + 1
if index == 1 then
user:setQuestProgress(235, 798)
user:inform(myTexts.red.german, myTexts.red.english)
elseif index == 2 then
user:setQuestProgress(235, 10)
user:inform(myTexts.blue.german, myTexts.blue.english)
end
end
local dialog = SelectionDialog(
common.GetNLS(user, myTexts.name.german, myTexts.name.english),
common.GetNLS(user, myTexts.selectColour.german, myTexts.selectColour.english),
callback)
dialog:addOption(0, common.GetNLS(user, myTexts.colours.red.german, myTexts.colours.red.english))
dialog:addOption(0, common.GetNLS(user, myTexts.colours.blue.german, myTexts.colours.blue.english))
dialog:setCloseOnMove()
user:requestSelectionDialog(dialog)
end
local function teleport(user, actionState, portal, destination)
local wear = getPortalWear(user)
local thePos = user.pos
local portalType = user:getQuestProgress(235)
if portalType == 0 then
portalType = 10
end
if portal then
thePos = common.GetFrontPosition(user)
end
local castDuration = getCastTime(user)
world:gfx(41, thePos)
world:makeSound(13, thePos)
removeUsedMana(user)
magic.wandDegrade(user)
user:learn(Character.spatialMagic, castDuration, 100)
if portal then
if not world:getField(thePos) or (world:isItemOnField(thePos) and world:getItemOnField(thePos).wear == 255) then
--[[
Only checks for static items here.
This way if the player turned around to try and cheat the portal placement,
it still stops from casting on static. On the other hand, if another player
tossed an item before them to troll the mage mid cast, it ignores it and just
places it ontop of the item.
]]
user:inform(myTexts.locationBlocked.german, myTexts.locationBlocked.english)
elseif antiTroll.passesAntiTrollCheck(thePos) then
local thePortal = world:createItemFromId(portalType, 1, thePos, true, 999, {destinationCoordsZ = destination.z, destinationCoordsY = destination.y, destinationCoordsX = destination.x})
thePortal.wear = wear
world:changeItem(thePortal)
logPlayer(user.name.."("..user.id..")".." creates portal to "..tostring(destination))
if tutorials.isTutorialNPCnearby(user) then
if user:getQuestProgress(240) == 5 then
user:setQuestProgress(240, 6)
end
end
else
user:inform(myTexts.badTarget.german, myTexts.badTarget.english)
end
else
if tutorials.isTutorialNPCnearby(user) then
if user:getQuestProgress(240) == 3 then
user:setQuestProgress(240, 4)
end
end
logPlayer(user.name.."("..user.id..")".." teleporting to "..tostring(destination))
user:warp(destination)
end
end
function M.startCycle(user, actionState, oralCast)
local attunement = M[user.name.."attunement"]
if M[user.name.."cycles"] > 0 then
local castDuration = getCastTime(user)
M[user.name.."cycles"] = M[user.name.."cycles"] - 1
local location = user.pos
local portal = M[user.name.."portal"]
if portal then
location = common.GetFrontPosition(user)
end
magic.pickAndPlayRandomGfx(location)
user:performAnimation(globalvar.charAnimationSPELL)
world:makeSound(globalvar.sfxSNARING, user.pos)
user:startAction(math.ceil(castDuration/startCycles), 21, 10, 0, 0)
elseif attunement then
M.attuneSpot(user, attunement)
user:inform(myTexts.doneAttuning.german, myTexts.doneAttuning.english)
M[user.name.."attunement"] = false
else
local destination = M[user.name.."destination"]
local portal = M[user.name.."portal"]
teleport(user, actionState, portal, destination, oralCast)
end
end
local function checkForWand(user)
local wand = common.getItemInHand(user, magic.wandIds)
if not wand then return true end --You can cast spatial magic without a wand just without the boni of wielding one
if common.isBroken(wand) then
user:inform("Dein Zauberstab ist zerbrochen. Du solltest ihn reparieren lassen, bevor du versuchst, ihn zu benutzen.", "Your wand is broken. You should see to its repairs before trying to use it.")
return false
end
return true
end
local function closePortal(user, actionState)
if not magic.hasSufficientMana(user, 1000) then
user:inform("Du hast nicht genug Mana.", "You do not have enough mana.")
return
end
local front = common.GetFrontPosition(user)
local portal = false
if world:isItemOnField(front) then
local possiblePortal = world:getItemOnField(front)
if possiblePortal.id == 10 and possiblePortal.wear ~= 255 then
portal = possiblePortal
end
end
if not portal then
user:inform("Es gibt kein Portal, das du schließen kannst. Es muss vor dir sein.", "There's no portal for you to close. It needs to be in front of you.")
return
end
if actionState == Action.none then
user:startAction(30, 21, 10, 0, 0)
elseif actionState == Action.success then
user:increaseAttrib("mana", -1000)
world:erase(portal,1)
world:gfx(45,portal.pos)
end
end
local function chooseLocation(user, actionState, portal, spokenWords)
if not checkIfEnoughMana(user) then
user:inform("Du hast nicht genug Mana.", "You do not have enough mana.")
return
end
if not antiTroll.passesAntiTrollCheck(common.GetFrontPosition(user)) then
user:inform(myTexts.badTarget.german, myTexts.badTarget.english)
return
end
if portal and world:isItemOnField(common.GetFrontPosition(user)) then
user:inform(myTexts.locationBlocked.german, myTexts.locationBlocked.english)
return
end
local destination
local spots = getListOfAttunedSpots(user, portal)
for _, spot in pairs(spots) do
if spokenWords and (string.find(spokenWords, spot.nameEn) or string.find(spokenWords, spot.nameDe)) then
M[user.name.."attunement"] = false -- If a previous attunement attempt was interrupted, it might still be stored, so we overwrite it here
M[user.name.."destination"] = spot.location
M[user.name.."portal"] = portal
M[user.name.."cycles"] = startCycles
M.startCycle(user)
return
end
end
local callback = function (dialog)
if (not dialog:getSuccess()) then
return
end
local index = dialog:getSelectedIndex() + 1
for i = 1, #spots do
if index == i then
destination = spots[i].location
M[user.name.."attunement"] = false -- If a previous attunement attempt was interrupted, it might still be stored, so we overwrite it here
M[user.name.."destination"] = destination
M[user.name.."portal"] = portal
M[user.name.."cycles"] = startCycles
M.startCycle(user)
end
end
end
local dialog = SelectionDialog(
common.GetNLS(user, myTexts.teleportation.german, myTexts.teleportation.english),
common.GetNLS(user, myTexts.destination.german, myTexts.destination.english),
callback)
for i = 1, #spots do
dialog:addOption(0, common.GetNLS(user, spots[i].nameDe, spots[i].nameEn))
end
dialog:setCloseOnMove()
user:requestSelectionDialog(dialog)
end
local function portalMenu(user, ltstate)
local callback = function (dialog)
if (not dialog:getSuccess()) then
return
end
local index = dialog:getSelectedIndex() + 1
if index == 1 then
local incantation = myTexts.incantation.attune
user:talk(Character.say, incantation)
return
elseif index == 2 then
local incantation = myTexts.incantation.teleport
user:talk(Character.say, incantation)
return
elseif index == 3 then
local incantation = myTexts.incantation.portal
user:talk(Character.say, incantation)
return
elseif index == 4 then
M.showBookQuality(user)
elseif index == 5 then
local incantation = myTexts.incantation.close
user:talk(Character.say, incantation)
elseif index == 6 then
M.selectPortalColor(user)
return
end
end
local dialog = SelectionDialog(
common.GetNLS(user, myTexts.name.german, myTexts.name.english),
common.GetNLS(user, myTexts.castSelect.german, myTexts.castSelect.english),
callback)
dialog:addOption(0,common.GetNLS(user, myTexts.attuneTo.german, myTexts.attuneTo.english))
dialog:addOption(0, common.GetNLS(user, myTexts.teleport.german, myTexts.teleport.english))
dialog:addOption(0, common.GetNLS(user, myTexts.portal.german, myTexts.portal.english))
dialog:addOption(0,common.GetNLS(user, myTexts.showBookQuality.german, myTexts.showBookQuality.english))
dialog:addOption(0,common.GetNLS(user, myTexts.closePortal.german, myTexts.closePortal.english))
if user:getQuestProgress(225) ~= 0 then
dialog:addOption(0, common.GetNLS(user, myTexts.portalColour.german, myTexts.portalColour.english))
end
dialog:setCloseOnMove()
user:requestSelectionDialog(dialog)
end
local function skipPortalMenu(user, actionState, incantation, spokenWords)
if incantation == myTexts.incantation.teleport then
chooseLocation(user, actionState, false, spokenWords)
elseif incantation == myTexts.incantation.portal then
chooseLocation(user, actionState, true, spokenWords)
elseif incantation == myTexts.incantation.attune then
M.checkSpotEligiblity(user)
elseif incantation == myTexts.incantation.close then
closePortal(user, actionState)
end
end
function M.castSpatialMagic(user, actionState, oralCast, spokenWords)
if not checkForWand(user) then return end
if actionState == Action.none then
if not oralCast then
portalMenu(user, actionState)
else
skipPortalMenu(user, actionState, oralCast.spatial, spokenWords)
end
elseif actionState == Action.abort then
user:inform(common.GetNLS(user, myTexts.interruptedCast.german, myTexts.interruptedCast.english))
return
elseif actionState == Action.success then
if oralCast.spatial == myTexts.incantation.close then
closePortal(user, actionState)
else
M.startCycle(user, actionState, oralCast)
end
end
end
--GM tool functions:
function M.chooseLocationToAttune(user, target)
local callback = function (dialog)
if (not dialog:getSuccess()) then
return
end
local index = dialog:getSelectedIndex() + 1
local skippedSpots = 0
for i = 1, #portalSpots do
if M.spotAttuned(target, i) then
skippedSpots = skippedSpots+1
elseif index == i - skippedSpots then
M.attuneSpot(target, i)
user:inform("You attuned "..target.name.." to the portal location of "..portalSpots[i]["nameEn"])
user:logAdmin(user.name.." has attuned "..target.name.." to the portal location of "..portalSpots[i]["nameEn"])
end
end
end
local dialog = SelectionDialog("Runes", "Select a rune to teach", callback)
local unknownSpots = 0
for i = 1, #portalSpots do
if not M.spotAttuned(target, i) then
dialog:addOption(0, portalSpots[i]["nameEn"])
unknownSpots = unknownSpots +1
end
end
if unknownSpots > 0 then
dialog:setCloseOnMove()
user:requestSelectionDialog(dialog)
else
user:inform(target.name.." is already attuned to all spots.")
end
end
function M.attuneAllLocations(user, target)
for i = 1, #portalSpots do
if not M.spotAttuned(target, i) then
M.attuneSpot(target, i)
end
end
user:inform(target.name.." has been attuned to all portal spots.")
user:logAdmin(user.name.." has attuned all portal spots "..target.name)
end
return M