illarion-content/monster/base/quests.lua
2021-04-07 12:44:45 +02:00

396 lines
16 KiB
Lua
Raw Permalink Blame History

--[[
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/>.
]]
--Generic 'Kill X monsters' quests originally by Estralis Seborian
local common = require("base.common")
local M = {}
--[[Example of an entry in a quest script:
monsterQuests.addQuest{
questId = 4,
location = {position = position(775, 475, 0), radius = 75},
queststatus = {from = 1, to = 7},
questTitle = {german = "Der Wilderer I", english = "The Poacher I"},
monsterName = {german = "Kaninchen", english = "Rabbits"},
npcName = "Eugene Burton",
customQuestInform = {german = "Text", english = "Text"},
raceIds = {113} -- all rabbits
or
monsterGroupIds = {6} -- all drows
or
monsterIds = {35} -- elven ranger
}
questId: ID of the quest
location: A position around which the monsters have to be slain, e.g. centre of a dungeon or forest
radius: Within this radius around the questlocation, kills are counted valid
queststatus: Quest is only active between "from" and "to". Each monster slain adds +1. Use a value > 0! Quest is finished if "to" queststatus is reached, no kills are counted anymore. Difference between "from" and "to" is the number of monsters that have to be slain
questTitle: Title of the quest in German and English
monsterName: Free description of the foes in German and English
npcName: This is the name of the NPC who gives out the quest
customQuestInform: To add a custom quest inform, instead of the default inform, at the end of the quest
raceIds: All monsters of a certain race
monsterGroupIds: For a group of monsters (6 means: 61 thru 70)
monsterIds: For specific monsters
Comment: If you want an NPC to give out multiple quests, you can do it like this:
Quest 1: To accept quest 1, set queststatus to 1 with the NPC.
Use queststatus 1->11 to count 10 monsters. If the quest is finished, set queststatus to 12 with the NPC.
Quest 2: To accept quest 2, set queststatus to 13 with the NPC.
Use queststatus 13->18 to count 5 monsters. If the quest is finished, set queststatus to 19 with the NPC.
Quest 3: To accept quest 3, set queststatus to 20 with the NPC.
Use queststatus 20->21 to count 1 monster. If the quest is finished, set queststatus to 22 with the NPC.
]]
local quests = {}
local questsByMonsterId = {}
local questsByMonsterGroupId = {}
local questsByRaceId = {}
local function _isNumber(value)
return type(value) == "number"
end
local function _isTable(value)
return type(value) == "table"
end
local function _isFunction(value)
return type(value) == "function"
end
local function _isString(value)
return type(value) == "string"
end
local function registerQuest(quest, monsterIds, groupIds, raceIds)
if not _isTable(quest) then
error("Tried to insert a illegal structure as quest: Not a table")
end
if not _isFunction(quest.check) or not _isFunction(quest.execute) then
error("Tried to insert a illegal structure as quest: Required check and execute functions not present")
end
table.insert(quests, quest)
local index = #quests
if monsterIds ~= nil then
if _isTable(monsterIds) then
for _, id in pairs(monsterIds) do
local numberId = tonumber(id)
if questsByMonsterId[numberId] == nil then
questsByMonsterId[numberId] = {}
end
table.insert(questsByMonsterId[numberId], index)
end
else
error("The list of monster IDs is expected to be a table of IDs.")
end
end
if groupIds ~= nil then
if _isTable(groupIds) then
for _, id in pairs(groupIds) do
local numberId = tonumber(id)
if questsByMonsterGroupId[numberId] == nil then
questsByMonsterGroupId[numberId] = {}
end
table.insert(questsByMonsterGroupId[numberId], index)
end
else
error("The list of group IDs is expected to be a table of IDs.")
end
end
if raceIds ~= nil then
if _isTable(raceIds) then
for _, id in pairs(raceIds) do
local numberId = tonumber(id)
if questsByRaceId[numberId] == nil then
questsByRaceId[numberId] = {}
end
table.insert(questsByRaceId[numberId], index)
end
else
error("The list of race IDs is expected to be a table of IDs.")
end
end
end
function M.addQuest(params)
if not _isTable(params) then
error("Quest data for the quest to add is missing.")
end
local questId
if not _isNumber(params.questId) then
error("The quest ID is required to be a number. It is the ID of the quest progress.")
else
questId = tonumber(params.questId)
end
local checkQuest
if _isFunction(params.check) then
checkQuest = params.check
else
local questLocations
if _isTable(params.location) then
questLocations = {params.location}
elseif _isTable(params.locations) then
questLocations = params.locations
else
error("One or multiple locations for the quests are required.")
end
for _, location in pairs(questLocations) do
if not _isTable(location) then
error("Location is not properly stored in a table.")
end
if location.position == nil or not _isNumber(location.radius) then
error("Each location definition requires a position and a radius to be valid.")
end
end
local minimalStatus, maximalStatus
if _isTable(params.queststatus) then
if _isNumber(params.queststatus.from) and _isNumber(params.queststatus.to) then
minimalStatus = tonumber(params.queststatus.from)
maximalStatus = tonumber(params.queststatus.to)
elseif _isNumber(params.queststatus.min) and _isNumber(params.queststatus.max) then
minimalStatus = tonumber(params.queststatus.min)
maximalStatus = tonumber(params.queststatus.max)
elseif _isNumber(params.queststatus[1]) and _isNumber(params.queststatus[2]) then
minimalStatus = tonumber(params.queststatus[1])
maximalStatus = tonumber(params.queststatus[2])
else
error("Failed to read the quest status range.")
end
elseif _isNumber(params.queststatus) then
minimalStatus = tonumber(params.queststatus)
maximalStatus = tonumber(params.queststatus)
else
error("Failed to read the required queststatus parameter.")
end
if minimalStatus > maximalStatus or minimalStatus < 0 then
error("Quest status values are out of range.")
end
checkQuest = function(player, monster)
if not player:isInRange(monster, 12) then
-- Monster is too far away. Something went wrong
return false
end
for _, location in pairs(questLocations) do
if player:isInRangeToPosition(location.position, location.radius) then
local currentStatus = player:getQuestProgress(questId);
return currentStatus >= minimalStatus and currentStatus < maximalStatus
end
end
return false
end
end
local executeQuest
if _isFunction(params.execute) then
executeQuest = params.execute
else
executeQuest = function(player, monster)
local currentStatus = player:getQuestProgress(questId);
player:setQuestProgress(questId, currentStatus + 1)
end
end
local reportQuest
if _isFunction(params.report) then
reportQuest = params.report
else
local minimalStatus, maximalStatus
if _isTable(params.queststatus) then
if _isNumber(params.queststatus.from) and _isNumber(params.queststatus.to) then
minimalStatus = tonumber(params.queststatus.from)
maximalStatus = tonumber(params.queststatus.to)
elseif _isNumber(params.queststatus.min) and _isNumber(params.queststatus.max) then
minimalStatus = tonumber(params.queststatus.min)
maximalStatus = tonumber(params.queststatus.max)
elseif _isNumber(params.queststatus[1]) and _isNumber(params.queststatus[2]) then
minimalStatus = tonumber(params.queststatus[1])
maximalStatus = tonumber(params.queststatus[2])
else
error("Failed to read the quest status range.")
end
elseif _isNumber(params.queststatus) then
minimalStatus = tonumber(params.queststatus)
maximalStatus = tonumber(params.queststatus)
else
error("Failed to read the required queststatus parameter.")
end
if minimalStatus > maximalStatus or minimalStatus < 0 then
error("Quest status values are out of range.")
end
local titleGerman, titleEnglish
if _isTable(params.questTitle) then
if _isString(params.questTitle.german) and _isString(params.questTitle.english) then
titleGerman = params.questTitle.german
titleEnglish = params.questTitle.english
elseif _isString(params.questTitle[Player.german]) and _isString(params.questTitle[Player.english]) then
titleGerman = params.questTitle[Player.german]
titleEnglish = params.questTitle[Player.english]
end
elseif _isString(params.questTitle) then
titleGerman = params.questTitle
titleEnglish = params.questTitle
else
error("Failed to read the required title of the quest.")
end
local monsterNameGerman, monsterNameEnglish
if _isTable(params.monsterName) then
local germanParams = params.monsterName.german or params.monsterName[Player.german]
local englishParams = params.monsterName.english or params.monsterName[Player.english]
if _isString(germanParams) then
monsterNameGerman = germanParams
else
error("Failed to read the german part of the monster name.")
end
if _isString(englishParams) then
monsterNameEnglish = englishParams
else
error("Failed to read the english part of the monster name.")
end
elseif _isString(params.monsterName) then
monsterNameGerman = params.monsterName
monsterNameEnglish = params.monsterName
else
error("Failed to read the required name of the monster.")
end
local npcName
if _isString(params.npcName) then
npcName = params.npcName
else
error("Failed to read the required NPC name.")
end
local customQuestInformGerman, customQuestInformEnglish
if _isTable(params.customQuestInform) then
local germanCQI = params.customQuestInform.german or params.customQuestInform[Player.german]
local englishCQI = params.customQuestInform.english or params.customQuestInform[Player.english]
if _isString(germanCQI) then
customQuestInformGerman = germanCQI
else
error("Failed to read the german part of the quest Inform.")
end
if _isString(englishCQI) then
customQuestInformEnglish = englishCQI
else
error("Failed to read the english part of the quest Inform.")
end
else
if _isString(params.customQuestInform) then
customQuestInformGerman = params.customQuestInform
customQuestInformEnglish = params.customQuestInform
end
end
local totalCount = maximalStatus - minimalStatus
reportQuest = function(player, monster)
local currentStatus = player:getQuestProgress(questId);
if currentStatus >= maximalStatus then --quest finished
local germanFormat, englishFormat
if maximalStatus == minimalStatus + 1 then -- only a single monster to beat
germanFormat = "[Queststatus] %s: Du hast %s besiegt. \z
Kehre zu %s zur<75>ck, um deine Belohnung zu erhalten."
englishFormat = "[Quest status] %s: You have slain %s. Return to %s to claim your reward."
else
germanFormat = "[Queststatus] %s: Du hast genug %s besiegt. \z
Kehre zu %s zur<75>ck, um deine Belohnung zu erhalten."
englishFormat = "[Quest status] %s: You have slain enough %s. Return to %s to claim your reward."
end
if _isTable(params.customQuestInform) then
common.InformNLS(player,customQuestInformGerman,customQuestInformEnglish);
else
common.InformNLS(player,
germanFormat:format(titleGerman, monsterNameGerman, npcName),
englishFormat:format(titleEnglish, monsterNameEnglish, npcName));
end
else --quest not finished
local germanFormat = "[Queststatus] %s: Du hast %d von %d %s besiegt."
local englishFormat = "[Quest status] %s: You have slain %d of %d %s."
local doneCount = currentStatus - minimalStatus
common.InformNLS(player,
germanFormat:format(titleGerman, doneCount, totalCount, monsterNameGerman),
englishFormat:format(titleEnglish, doneCount, totalCount, monsterNameEnglish));
end
end
end
local quest = {}
function quest.check(player, monster)
return checkQuest(player, monster)
end
function quest.execute(player, monster)
executeQuest(player, monster)
reportQuest(player, monster)
end
registerQuest(quest, params.monsterIds, params.monsterGroupIds, params.raceIds)
end
function M.checkQuest(player, monster)
local monsterId = monster:getMonsterType()
local monsterGroupId = math.floor((monsterId - 1) / 10)
local raceId = monster:getRace()
local possibleQuests = {}
if questsByMonsterId[monsterId] ~= nil then
for _, questId in pairs(questsByMonsterId[monsterId]) do
possibleQuests[questId] = true
end
end
if questsByMonsterGroupId[monsterGroupId] ~= nil then
for _, questId in pairs(questsByMonsterGroupId[monsterGroupId]) do
possibleQuests[questId] = true
end
end
if questsByRaceId[raceId] ~= nil then
for _, questId in pairs(questsByRaceId[raceId]) do
possibleQuests[questId] = true
end
end
-- All candidates are assembled. Now check the ones that execute.
local checkedQuests = {}
for questId, _ in pairs(possibleQuests) do
local quest = quests[questId]
if quest.check(player, monster) then
table.insert(checkedQuests, quest)
end
end
for _, quest in pairs(checkedQuests) do
quest.execute(player, monster)
end
end
return M