illarion-content/npc/base/basic.lua
2021-06-06 15:21:52 +02:00

547 lines
19 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/>.
]]
--- Base NPC script for basic NPCs
--
-- This script bundles the functions the most other specific components of the
-- NPC require to work. Its the basic script of the NPC framework and the only
-- one that requires regular from the actual NPC script.
--
-- Author: Martin Karing
local common = require("base.common")
local class = require("base.class")
local mutedNPCs = {}
local langCodeToSkill
local displayLanguageConfusion
local isAdminOnField
--- Constructor for the baseNPC. This does not take any paramters.
--
-- The sole purpose is to prepare all required values in the NPC script.
local baseNPC = class(function(self)
--- Constant for the state value of the NPC.
--- If this constant is set the NPC is currently in the normal mode.
self.stateNormal = 0
--- Constant for the state value of the NPC.
--- If this constant is set the NPC is currently busy talking with a specified
--- character and is not to walk around or anything like this.
self.stateBusyTalking = 1
-- The state of the NPC. This value can be used to have the special parts
-- of the NPC communicating with each other.
self["state"] = self.stateNormal
-- The cycle functions are called during the next cycle method of this base
-- NPC struct. Each special NPC class is able to register functions in this
-- class.
self["_cycleFunctions"] = nil
-- The receive text functions are called during the receive text method of
-- the NPC. Each special NPC class is able to register functions in this
-- class.
self["_receiveTextFunctions"] = nil
-- The cycle counter counts the calls of the next cycle method in order to
-- find out when to execute the next time all the next cycle methods. This
-- is used to reduce the server load caused by this method.
self["_cycleCounter"] = 0
-- This variable stores then to call the cycle methods the next time in
-- order to execute all functions of the NPC properly.
self["_nextCycleCalls"] = 0
-- This variable holds a list that stores the constants of the languages
-- the NPC is able to use.
self["_npcLanguages"] = {}
-- This variable holds the amount of languages added to the list of
-- languages of this NPC.
self["_npcLanguageCount"] = 0
-- This variable stores the language the NPC is expected to use by default.
self["_defaultLanguage"] = 0
-- This variable stores the german lookAt message of the NPC.
self["_lookAtMsgDE"] = ""
-- This variable stores the english lookAt message of the NPC.
self["_lookAtMsgUS"] = ""
-- This variable stores the german message that is said by the NPC in case
-- the player performs a use operation on the NPC.
self["_useMsgDE"] = ""
-- This variable stores the english message that is said by the NPC in case
-- the player performs a use operation on the NPC.
self["_useMsgUS"] = ""
-- This variable stores the german message that is shown in case the players
-- talk to the NPC in a language the NPC does not understand.
self["_confusedDE"] = ""
-- This variable stores the english message that is shown in case the
-- players talk to the NPC in a language the NPC does not understand.
self["_confusedUS"] = ""
-- This list is used to store the equipment that shall be set to the NPC at
-- the first run. Once the equipment is set, the list is destroyed.
self["_equipmentList"] = {}
-- The town this NPC is affiliated to.
self["_affiliation"] = 0
end)
--- This method adds one function to the functions that are called during the
--- next cycles.
--
-- @param receiver the receiver is a object that defines a nextCycle function
-- that is called in given time intervals. The only paramter
-- of this function is the time since the last call. The return
-- value has to be the time when the next call of this function
-- is expected. Its possible that the function is called ealier
-- but not later.
function baseNPC:addCycle(receiver)
if (type(receiver) ~= "table") then
return
end
if (type(receiver.nextCycle) ~= "function") then
return
end
if (self._cycleFunctions == nil) then
self._cycleFunctions = {}
end
table.insert(self._cycleFunctions, receiver)
end
--- This method adds one function to the functions that are called during when
--- a text is received by the NPC.
--
-- @param receiver the receiver is a object that defines a receiveText function
-- that is called all times the basic npc receives a text. The
-- receive text function has to take as first paramter the
-- character who is talking and as second parameter the text
-- that was spoken. In case the receiveText function returns
-- true the spoken text is not forwarded to any more possible
-- receivers.
function baseNPC:addRecvText(receiver)
if (type(receiver) ~= "table") then
return
end
if (type(receiver.receiveText) ~= "function") then
return
end
if (self._receiveTextFunctions == nil) then
self._receiveTextFunctions = {}
end
table.insert(self._receiveTextFunctions, receiver)
end
--- This method has to be called during each next cycle call of an NPC. It
--- manages and calls the functions registered this NPC. This function will be
--- removed during its first call. Instead the function originally named
--- baseNPC:nextCycle2() will take its place and serve its actual purpose.
--
-- @param npcChar the NPC character
function baseNPC:nextCycle(npcChar)
if (self.initLanguages ~= nil) then
self:initLanguages(npcChar)
end
if (self._equipmentList ~= nil) then
local tempList = self._equipmentList
self["_equipmentList"] = nil
for _, value in pairs(tempList) do
npcChar:createAtPos(value[1], value[2], 1)
local item = npcChar:getItemAt(value[1])
item.wear = 255
item.quality = 999
world:changeItem(item)
end
end
self.nextCycle = self.nextCycle2
self.nextCycle2 = nil
end
--- This method has to be called during each next cycle call of an NPC. It
--- manages and calls the functions registered this NPC. This function will be
--- copied to baseNPC:nextCycle() once the initialization is done.
--
-- @param npcChar the NPC character
function baseNPC:nextCycle2(npcChar)
if (self._cycleFunctions == nil) then
return
end
if isAdminOnField(npcChar.pos) then
return
end
if (self._cycleCounter < self._nextCycleCalls) then
self._cycleCounter = self._cycleCounter + 1
return
end
npcChar.activeLanguage = self._defaultLanguage
local oldCycle = self._cycleCounter
self._cycleCounter = 0
self._nextCycleCalls = 2147483648
for _, value in pairs(self._cycleFunctions) do
local nextRequestedCall = value:nextCycle(npcChar, oldCycle)
if (nextRequestedCall ~= nil and nextRequestedCall >= 0) then
self._nextCycleCalls = math.min(self._nextCycleCalls, nextRequestedCall)
end
end
end
--- This method should be called at each call of the receive text method. This
--- method will maintain all functions registered to this class and call them
--- properly in case its needed.
--
-- @param npcChar the NPC character
-- @param textType the type that was used to send the text
-- @param speaker the character struct who said some text
-- @param text the text that was spoken
-- @return true in case the text was handled properly by one of the receive text handlers
function baseNPC:receiveText(npcChar, texttype, speaker, text)
if not npcChar:isInRange(speaker, 2) then
return false
end
if speaker:isAdmin() then
local npcName = npcChar.name
if string.find(text, "unmute") then
mutedNPCs[npcChar.id] = nil
speaker:inform("You unmute " .. npcName)
return false
elseif string.find(text, "mute") then
mutedNPCs[npcChar.id] = true
speaker:inform("You mute " .. npcName)
return false
end
end
if mutedNPCs[npcChar.id] == true then
local npcName = npcChar.name
speaker:inform(npcName .. " scheint heute nicht sehr gespr<70>chig." , npcName .. " doesn't seem to be in a chatty mood today.")
return false
end
if (self._receiveTextFunctions == nil) then
return false
end
if isAdminOnField(npcChar.pos) then
return false
end
if not npcChar:isInRange(speaker, 2) then
return false
end
if (speaker.id == npcChar.id) then
return false
end
if (speaker:getType() ~= 0) then
return false
end
if not self:checkLanguageOK(speaker) then
displayLanguageConfusion(npcChar, self)
return false
end
npcChar.activeLanguage = speaker.activeLanguage
text = string.lower(text)
for _, value in pairs(self._receiveTextFunctions) do
if (value:receiveText(npcChar, texttype, speaker, text)) then
return true
end
end
return false
end
--- This function checks if the language currently spoken by a character is
--- valid to be used by this NPC.
--
-- @param speak the character who is speaking
-- @return true in case the NPC is able to speak the language of the player
function baseNPC:checkLanguageOK(speaker)
if (speaker.activeLanguage == self._defaultLanguage) then
return true
end
for i=1, self._npcLanguageCount do
if (speaker.activeLanguage == self._npcLanguages[i]) then
return true
end
end
return false
end
--- This function adds a language constant to the list of enabled languages of
--- this NPC.
--
-- @param langCode the code of the language the NPC is supposed to know
function baseNPC:addLanguage(langCode)
table.insert(self._npcLanguages, langCode)
self._npcLanguageCount = self._npcLanguageCount + 1
end
--- This function sets the language the NPC is supposed to use by default. The
--- only case it is switching the language is, in case he answeres to a player
--- talking in another language.
--
-- @param langCode the code value of the language to use
function baseNPC:setDefaultLanguage(langCode)
self._defaultLanguage = langCode
end
--- This function sets the texts that are displayed when someone is looking at
--- the NPC.
--
-- @param german the german message
-- @param english the english message
function baseNPC:setLookat(german, english)
self._lookAtMsgDE = german
self._lookAtMsgUS = english
end
--- This function sets the text that are displayed when someone is using the
--- NPC.
--
-- @param german the german message
-- @param english the english message
function baseNPC:setUseMessage(german, english)
self._useMsgDE = german
self._useMsgUS = english
end
--- This function sets the text that is displayed in case a player is talking
--- in the wrong language with the NPC.
--
-- @param german the german message
-- @param english the english message
function baseNPC:setConfusedMessage(german, english)
self._confusedDE = german
self._confusedUS = english
end
--- This method handles the lookat requests that are send to the NPC. If set
--- properly a message will be returned describing the appearance of the NPC.
--
-- @param npcChar the NPC character
-- @param char the character who is looking at the NPC
-- @param mode the mode used to look at the NPC (no effect)
function baseNPC:lookAt(npcChar, char, mode)
char:sendCharDescription(npcChar.id, common.GetNLS(char, self._lookAtMsgDE, self._lookAtMsgUS))
end
--- This method handles all use methods that are done to the NPC. When ever a
--- NPC is used by the player this one is called.
--
-- @param npcChar the NPC character
-- @param char the character who is looking at the NPC
-- @param mode the mode used to look at the NPC (no effect)
function baseNPC:use(npcChar, char)
if char:isAdmin() then
local lastSpoken = char.lastSpokenText
if string.find(string.lower(lastSpoken), "teleport") then
local coords = {}
for coord in lastSpoken:gmatch("%-?%d+") do
table.insert(coords, tonumber(coord))
end
if coords[1] and coords[2] and coords[3] then
npcChar:forceWarp(position(coords[1], coords[2], coords[3]))
end
end
end
if mutedNPCs[npcChar.id] == true then
local npcName = npcChar.name
char:inform(npcName .. " scheint heute nicht sehr gespr<70>chig." , npcName .. " doesn't seem to be in a chatty mood today.")
return
end
if isAdminOnField(npcChar.pos) then
return
end
npcChar.activeLanguage = self._defaultLanguage
local getText = function(deText,enText) return common.GetNLS(char, deText, enText) end
local callback = function(dialog)
local success = dialog:getSuccess()
if success then
local selected = dialog:getSelectedIndex()
if selected == 5 then --free input
local callbackInput = function(dialogInput)
if not dialogInput:getSuccess() then
return;
else
local text = dialogInput:getInput()
char:talk(Character.say, text, text)
end
end
local dialogInput
dialogInput = InputDialog(npcChar.name, getText("<EFBFBD>ber welches Thema m<>chtest du sprechen?","Select the topic you want to talk about."), false, 255, callbackInput)
char:requestInputDialog(dialogInput)
else
local textDE={}
local textEN={}
textDE[0]="Seid gegr<67><72>t."
textEN[0]="Be greeted."
textDE[1]="Ich ben<65>tige Hilfe."
textEN[1]="Please help me."
textDE[2]="Habt Ihr eine Aufgabe f<>r mich?"
textEN[2]="Do you have a task for me?"
textDE[3]="Ich m<>chte Waren handeln."
textEN[3]="I'd like to trade some goods."
textDE[4]="Auf wiedersehen."
textEN[4]="Farewell."
char:talk(Character.say, getText(textDE[selected], textEN[selected]))
end
end
end
local dialog = SelectionDialog(npcChar.name, getText("<EFBFBD>ber welches Thema m<>chtest du sprechen?","Select the topic you want to talk about."), callback)
dialog:setCloseOnMove()
dialog:addOption(0, getText("Begr<EFBFBD><EFBFBD>en", "Greetings"))
dialog:addOption(0, getText("Hilfe", "Help"))
dialog:addOption(0, getText("Quest", "Quest"))
dialog:addOption(0, getText("Handel", "Trade"))
dialog:addOption(0, getText("Verabschieden", "Farewell"))
dialog:addOption(0, getText("(Etwas anderes)", "(Other)"))
char:requestSelectionDialog(dialog)
end
--- This equipment sets the equipment an NPC gets at first start. This is needed
--- so the NPC looks good with paperdolling.
--
-- @param slot the slot the item is to be placed in
-- @param item the item ID that shall be created
function baseNPC:setEquipment(slot, item)
table.insert(self["_equipmentList"], {slot, item})
end
--- This function is used to set the affiliation of the NPC.
--
-- @param affiliation the index of the town this NPC is assigned to
function baseNPC:setAffiliation(affiliation)
self["_affiliation"] = affiliation
end
--- This is a cleanup function that should be called once the initialization of
--- the NPC is done. It will free the memory taken by all the functions that are
--- needed to fill data into the NPC.
function baseNPC:initDone()
self["addCycle"] = nil
self["addRecvText"] = nil
self["addLanguageCode"] = nil
self["setDefaultLanguage"] = nil
self["setLookat"] = nil
self["setUseMessage"] = nil
self["setConfusedMessage"] = nil
self["setEquipment"] = nil
self["initDone"] = nil
end
function baseNPC:setAutoIntroduceMode(autointroduce)
end
--- This function learns the NPC the languages skills needed to work properly.
--- Check if this function is not nil before you call it, because it destructs
--- itself after it was called once.
--
-- @param npcChar the NPC character
function baseNPC:initLanguages(npcChar)
for _, value in pairs(self._npcLanguages) do
npcChar:increaseSkill(langCodeToSkill(value), 100)
end
self["initLanguages"] = nil
end
--- This function translates a language code value to the skill that belongs to
--- this number. This skill name is needed to set the skill value of the NPC in
--- so the NPC is able to talk in the needed language perfectly.
--
-- @param langCode the language code
-- @return the skill fitting to the language code
function langCodeToSkill(langCode)
if (langCode == 0) then
return Character.commonLanguage
elseif (langCode == 1) then
return Character.humanLanguage
elseif (langCode == 2) then
return Character.dwarfLanguage
elseif (langCode == 3) then
return Character.elfLanguage
elseif (langCode == 4) then
return Character.lizardLanguage
elseif (langCode == 5) then
return Character.orcLanguage
elseif (langCode == 10) then
return Character.ancientLanguage
else
return Character.commonLanguage
end
end
--- This function is used to display the message that the NPC is not able to
--- understand the language the player character is speaking. It should be
--- called every time the languageOK check is failing because it has its own
--- protection against spamming.
--
-- @param npcChar the NPC character
-- @param npc the baseNPC instance that is referred to
function displayLanguageConfusion(npcChar, npc)
if not common.spamProtect(npcChar, 60) then
npcChar.activeLanguage = npc._defaultLanguage
npcChar:talk(Character.say, npc._confusedDE, npc._confusedUS)
end
end
-- check if there is a GM "possessing" the NPC
function isAdminOnField(pos)
local stackedChar = world:getCharacterOnField(pos)
if stackedChar:isAdmin() then
return true
end
return false
end
return baseNPC