• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

Lua [TFS 1.6+] Converting NPC's (Jiddos system) into new NPC System (Evil Heros system)

Evil Hero

Legacy Member
TFS Developer
Joined
Dec 12, 2007
Messages
1,281
Solutions
29
Reaction score
820
Location
Germany
For anyone who wants to use the new NPC system (lua based NPC's) proposed as a pr at current master branch (included in the upcoming 1.6 release)
I'll help you re write NPC's or create from scratch based on a detailed outline into the new NPC system.

There is 2 ways on how to request:

1) You post a file and I'll just 1:1 transfer it into the new system. (xml and lua file)
2) You write a very detailed version of the npc and what he should be capable of

You have to be prepared for them to be public domain, I'm not posting them in private and they will serve for other people as examples to understand how everything works.

For those who are interested into implementing this npc system into older tfs versions, here is the summarize of all commits which need to be applied:

Post automatically merged:

Let's start off with something very simple, just a Merchant in different versions
I'll try my best to make some good commentary with showing some visual branching on where we are at which stage.
Lua:
-- creates the new npc type with the name "Merchant"
local npcType = Game.createNpcType("Merchant")
-- sets the speech bubble of the npc to a trade bubble
npcType:speechBubble(SPEECHBUBBLE_TRADE)
-- sets the outfit of the npc
npcType:outfit({lookType = 128, lookHead = 114, lookBody = 114, lookLegs = 114, lookFeet = 114})
-- sets the radius on how far the npc can move away from his spawn position
npcType:spawnRadius(2)
-- sets the interval on when the npc should walk in ms
npcType:walkInterval(2000)
-- sets the movement speed on how fast the npc moves
npcType:walkSpeed(100)
-- this does all the magic in the background, initializing the npc system (onAppear/onDisappear/onSay/onTradeRequest, etc.)
npcType:defaultBehavior()
-- creates a new instance of the NpcsHandler class with the npcType
-- this holds all the tree & branch data for the npc
local handler = NpcsHandler(npcType)
-- creating the greeting keywords for the npc, handler.greetWords defaults to a table which can be found in constants.lua or can be overwritten by calling handler:setGreetWords({"word1", "word2", "word3"})
local greet = handler:keyword(handler.greetWords)
-- sets the response of the npc when the player greets him
greet:setGreetResponse("Hello |PLAYERNAME|! I have some fine {weapons} and {shields} for sale.")
--[[
    our current tree structure of the npc looks likes this
    greet
        |- nothing
       
    as we have no keywords after greeting yet
]]
-- creating the keyword "weapons" for the npc
-- this adds now a keyword onto the greet keyword
local weapons = greet:keyword("weapons")
-- sets the response of the npc when the player asks for weapons
weapons:respond("You want to see my fine weapons I offer?.")
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons

        we placed the keyword "weapons" onto greet
]]
-- creating the keyword "yes" which will now show us the shop with teh weapons
local accept = weapons:keyword("yes")
-- sets the response of the npc when the player says "yes"
accept:respond("Here are my weapons.")
-- this will show the shop with the id 1 now, I'll show at the end of the example on how to make a shop
accept:shop(1)
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
            |- yes
                |- show shop with id 1

        we placed the keyword "yes" onto weapons
]]
-- creating the keyword "no" which will now lead us back to the greet keyword tree (resetting the talk state)
local decline = weapons:keyword("no")
-- sets the response of the npc when the player says "no"
decline:respond("Ok then, not.")
-- if you wanted the npc now to directly ungreet the player you could call decline:farewell() here which would be like saying "bye" to the npc
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
            |- yes
            |    |- show shop with id 1
            |- no
                |- reset talk state

        we placed the keyword "no" onto weapons
]]
-- creating the keyword "shields" for the npc
local shields = greet:keyword("shields")
-- sets the response of the npc when the player asks for shields
shields:respond("You want to see my fine shields I offer?.")
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
        |   |- yes
        |   |   |- show shop with id 1
        |   |- no
        |      |- reset talk state to greet
        |- shields

        we placed the keyword "shields" onto greet
]]
-- creating the keyword "yes" which will now show us the shop with the shields
local accept = shields:keyword("yes")
-- sets the response of the npc when the player says "yes"
accept:respond("Here are my shields.")
-- this will show the shop with the id 2 now, I'll show at the end of the example on how to make a shop
accept:shop(2)
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
        |   |- yes
        |   |   |- show shop with id 1 (weapons)
        |   |- no
        |       |- reset talk state to greet
        |- shields
            |- yes
                |- show shop with id 2 (shields)

        we placed the keyword "yes" onto shields
]]
-- creating the keyword "no" which will now lead us back to the greet keyword tree (resetting the talk state)
local decline = shields:keyword("no")
-- sets the response of the npc when the player says "no"
decline:respond("Ok then, not.")
-- if you wanted the npc now to directly ungreet the player you could call decline:farewell() here which would be like saying "bye" to the npc
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
        |   |- yes
        |   |   |- show shop with id 1
        |   |- no
        |      |- reset talk state to greet
        |- shields
            |- yes
            |   |- show shop with id 2
            |- no
                |- reset talk state to greet

        we placed the keyword "no" onto shields
]]
-- creating the shop with the id 1 for the npc (shields)
local shop1 = NpcShop(npcType, 1)
-- example of adding a single item to the shop (id/name, buyPrice, sellPrice, subType)
shop1:addItem("sword", 100, 25)
-- example of adding multiple items to the shop (id/name, buyPrice, sellPrice, subType)
local exWeapons = {
    ["axe"] = {buy = 150, sell = 50},
    ["club"] = {buy = 50, sell = 10},
    ["dagger"] = {buy = 75, sell = 20},
    ["mace"] = {buy = 200, sell = 75},
    ["spear"] = {buy = 100, sell = 30},
    ["staff"] = {buy = 250, sell = 100},
    ["sword"] = {buy = 100, sell = 25}
}
shop1:addItems(exWeapons)
-- example of adding discount to the shop for a specific type of %
-- this will reduce the buy price of all items by 10% if the player has the storagevalue 9999 set to 1 or higher
shop1:addDiscount(9999, 10)
-- creating the shop with the id 2 for the npc (shields)
local shop2 = NpcShop(npcType, 2)
-- example of adding a single item to the shop (id/name, buyPrice, sellPrice, subType)
shop2:addItem("shield", 100, 25)
-- example of adding multiple items to the shop (id/name, buyPrice, sellPrice, subType)
local exShields = {
    ["shield"] = {buy = 100, sell = 25},
    ["wooden shield"] = {buy = 50, sell = 10},
    ["brass shield"] = {buy = 150, sell = 50},
    ["plate shield"] = {buy = 200, sell = 75},
    ["steel shield"] = {buy = 250, sell = 100}
}
shop2:addItems(exShields)
-- example of adding discount to the shop for a % depending on storagevalue 9998 value
-- this will reduce the buy price of all items by x% if the player has the storagevalue 9998 set between a number of 1 and 100
shop2:addDiscount(9998)
 
Last edited:
For anyone who wants to use the new NPC system (lua based NPC's) proposed as a pr at current master branch (included in the upcoming 1.6 release)
I'll help you re write NPC's or create from scratch based on a detailed outline into the new NPC system.

There is 2 ways on how to request:

1) You post a file and I'll just 1:1 transfer it into the new system. (xml and lua file)
2) You write a very detailed version of the npc and what he should be capable of

You have to be prepared for them to be public domain, I'm not posting them in private and they will serve for other people as examples to understand how everything works.
Post automatically merged:

Let's start off with something very simple, just a Merchant in different versions
I'll try my best to make some good commentary with showing some visual branching on where we are at which stage.
Lua:
-- creates the new npc type with the name "Merchant"
local npcType = Game.createNpcType("Merchant")
-- sets the speech bubble of the npc to a trade bubble
npcType:speechBubble(SPEECHBUBBLE_TRADE)
-- sets the outfit of the npc
npcType:outfit({lookType = 128, lookHead = 114, lookBody = 114, lookLegs = 114, lookFeet = 114})
-- sets the radius on how far the npc can move away from his spawn position
npcType:spawnRadius(2)
-- sets the interval on when the npc should walk in ms
npcType:walkInterval(2000)
-- sets the movement speed on how fast the npc moves
npcType:walkSpeed(100)
-- this does all the magic in the background, initializing the npc system (onAppear/onDisappear/onSay/onTradeRequest, etc.)
npcType:defaultBehavior()
-- creates a new instance of the NpcsHandler class with the npcType
-- this holds all the tree & branch data for the npc
local handler = NpcsHandler(npcType)
-- creating the greeting keywords for the npc, handler.greetWords defaults to a table which can be found in constants.lua or can be overwritten by calling handler:setGreetWords({"word1", "word2", "word3"})
local greet = handler:keyword(handler.greetWords)
-- sets the response of the npc when the player greets him
greet:setGreetResponse("Hello |PLAYERNAME|! I have some fine {weapons} and {shields} for sale.")
--[[
    our current tree structure of the npc looks likes this
    greet
        |- nothing
      
    as we have no keywords after greeting yet
]]
-- creating the keyword "weapons" for the npc
-- this adds now a keyword onto the greet keyword
local weapons = greet:keyword("weapons")
-- sets the response of the npc when the player asks for weapons
weapons:respond("You want to see my fine weapons I offer?.")
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons

        we placed the keyword "weapons" onto greet
]]
-- creating the keyword "yes" which will now show us the shop with teh weapons
local accept = weapons:keyword("yes")
-- sets the response of the npc when the player says "yes"
accept:respond("Here are my weapons.")
-- this will show the shop with the id 1 now, I'll show at the end of the example on how to make a shop
accept:shop(1)
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
            |- yes
                |- show shop with id 1

        we placed the keyword "yes" onto weapons
]]
-- creating the keyword "no" which will now lead us back to the greet keyword tree (resetting the talk state)
local decline = weapons:keyword("no")
-- sets the response of the npc when the player says "no"
decline:respond("Ok then, not.")
-- if you wanted the npc now to directly ungreet the player you could call decline:farewell() here which would be like saying "bye" to the npc
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
            |- yes
            |    |- show shop with id 1
            |- no
                |- reset talk state

        we placed the keyword "no" onto weapons
]]
-- creating the keyword "shields" for the npc
local shields = greet:keyword("shields")
-- sets the response of the npc when the player asks for shields
shields:respond("You want to see my fine shields I offer?.")
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
        |   |- yes
        |   |   |- show shop with id 1
        |   |- no
        |      |- reset talk state to greet
        |- shields

        we placed the keyword "shields" onto greet
]]
-- creating the keyword "yes" which will now show us the shop with the shields
local accept = shields:keyword("yes")
-- sets the response of the npc when the player says "yes"
accept:respond("Here are my shields.")
-- this will show the shop with the id 2 now, I'll show at the end of the example on how to make a shop
accept:shop(2)
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
        |   |- yes
        |   |   |- show shop with id 1 (weapons)
        |   |- no
        |       |- reset talk state to greet
        |- shields
            |- yes
                |- show shop with id 2 (shields)

        we placed the keyword "yes" onto shields
]]
-- creating the keyword "no" which will now lead us back to the greet keyword tree (resetting the talk state)
local decline = shields:keyword("no")
-- sets the response of the npc when the player says "no"
decline:respond("Ok then, not.")
-- if you wanted the npc now to directly ungreet the player you could call decline:farewell() here which would be like saying "bye" to the npc
--[[
    our current tree structure of the npc looks likes this
    greet
        |- weapons
        |   |- yes
        |   |   |- show shop with id 1
        |   |- no
        |      |- reset talk state to greet
        |- shields
            |- yes
            |   |- show shop with id 2
            |- no
                |- reset talk state to greet

        we placed the keyword "no" onto shields
]]
-- creating the shop with the id 1 for the npc (shields)
local shop1 = NpcShop(npcType, 1)
-- example of adding a single item to the shop (id/name, buyPrice, sellPrice, subType)
shop1:addItem("sword", 100, 25)
-- example of adding multiple items to the shop (id/name, buyPrice, sellPrice, subType)
local exWeapons = {
    ["axe"] = {buy = 150, sell = 50},
    ["club"] = {buy = 50, sell = 10},
    ["dagger"] = {buy = 75, sell = 20},
    ["mace"] = {buy = 200, sell = 75},
    ["spear"] = {buy = 100, sell = 30},
    ["staff"] = {buy = 250, sell = 100},
    ["sword"] = {buy = 100, sell = 25}
}
shop1:addItems(exWeapons)
-- example of adding discount to the shop for a specific type of %
-- this will reduce the buy price of all items by 10% if the player has the storagevalue 9999 set to 1 or higher
shop1:addDiscount(9999, 10)
-- creating the shop with the id 2 for the npc (shields)
local shop2 = NpcShop(npcType, 2)
-- example of adding a single item to the shop (id/name, buyPrice, sellPrice, subType)
shop2:addItem("shield", 100, 25)
-- example of adding multiple items to the shop (id/name, buyPrice, sellPrice, subType)
local exShields = {
    ["shield"] = {buy = 100, sell = 25},
    ["wooden shield"] = {buy = 50, sell = 10},
    ["brass shield"] = {buy = 150, sell = 50},
    ["plate shield"] = {buy = 200, sell = 75},
    ["steel shield"] = {buy = 250, sell = 100}
}
shop2:addItems(exShields)
-- example of adding discount to the shop for a % depending on storagevalue 9998 value
-- this will reduce the buy price of all items by x% if the player has the storagevalue 9998 set between a number of 1 and 100
shop2:addDiscount(9998)
This actually looks like an amazing system.

Is there a guide on how to add this npc system to the server?

I would then do it and start making some nice npc’s

Had a quick look.

Is this the only commit needed to do?

 
Last edited:
How about some callbacks?
Ex. shop2:addDiscount(9998) as callback:
  • returning float with price multiplier, not discount, so 10% discount is 0.9 of price (storage set to 90 = 90% of price)
  • make it able to apply multiple callbacks one after another
  • make it able to increase price, not only decrease
  • takes itemId as callback parameter, make it able to reduce/increase price of specific item
  • [optional feature] callback may return second parameter true / false ex. return 0.8, true, where second parameter would be stop, which would tell NPC system, if it should apply next price callbacks or stop (missing second parameter = nil -> false)
  • [optional feature] addPriceCallback may accept second parameter with callback priority, to make callbacks execute in some order
Lua:
shop2:addPriceCallback(function(player, itemId)
    if player:getStorage(9998) > 0 then
        return player:getStorage(9998) / 100
    end

    return 1.0
end)
Ex. with callback that reduces price by 20% for Magic Sword (2400) for players with storage 1234 set to 1:
Lua:
shop2:addPriceCallback(function(player, itemId)
    if player:getStorage(1234) == 1 and itemId == 2400 then
        return 0.8
    end

    return 1.0
end)
 
I have multiple questions about this npc system:
1. How would the oracle looks like
2. How would the banker looks like
3. How you make a shop where you can buy mana fluid in backpacks?
1) I'll make an oracle npc soon good one actually :D
3) I have not added a module for that one but I'll write it down, it could be achieved with callbacks tho (will include an example later on how to do that)

2) Banker NPC
Lua:
local npc = Game.createNpcType("Banker")
npc:speechBubble(SPEECHBUBBLE_TRADE)
npc:outfit({lookType = 472, lookHead = 40, lookBody = 95, lookLegs = 114, lookFeet = 27, addons = 1})
npc:defaultBehavior()
-- The NpcsHandler class is used to handle the NPC's responses to player interactions.
local handler = NpcsHandler(npc)
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse("Welcome to the bank, |PLAYERNAME|! Need some help with your {bank account}?")
local help = greet:keyword("help")
help:respond("You can check the {balance} of your bank account, {deposit} money or {withdraw} it. You can {transfer} money to other characters, provided that they have a vocation, or {change} the coins in your inventory.")
local bankAccount = greet:keyword("bank account")
bankAccount:respond("Would you like to know more about the {basic} functions of your bank account, the {advanced} functions, or are you already bored, perhaps?")
local basic = bankAccount:keyword({"basic", "functions", "job"})
basic:respond("I work in this bank. I can {change} money for you and help you with your {bank account}.")
local advanced = bankAccount:keyword("advanced")
advanced:respond("Your bank account will be used automatically when you want to {rent} a house or place an offer on an item on the {market}. Let me know if you want to know about how either one works.")
local rent = advanced:keyword("rent")
rent:respond("Once you have acquired a house the rent will be charged automatically from your {bank account} every month.")
local market = advanced:keyword("market")
market:respond("If you buy an item from the market, the required gold will be deducted from your bank account automatically. On the other hand, money you earn for selling items via the market will be added to your account. It's easy!")
-- Bank Balance
local balance = greet:keyword("balance")
function balance:callback(npc, player, message, handler)
    local balance = player:getBankBalance()
    if balance >= 100000000 then
        return true, "I think you must be one of the richest inhabitants in the world! Your account balance is " .. balance .. " gold."
    elseif balance >= 10000000 then
        return true, "You have made ten millions and it still grows! Your account balance is " .. balance .. " gold."
    elseif balance >= 1000000 then
        return true, "Wow, you have reached the magic number of a million gp!!! Your account balance is " .. balance.. " gold!"
    elseif balance >= 100000 then
        return true, "You certainly have made a pretty penny. Your account balance is " .. balance .. " gold."
    end
    return true, "Your account balance is " .. balance .. " gold."
end
-- Bank Deposit
local deposit = greet:keyword("deposit")
deposit:respond("How much money would you like to deposit?")
local answer = deposit:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getMoney() < money then
            return false, "You don't have enough money to deposit " .. money .. " gold."
        end
        handler:addData(player, "money", money)
        return true, "You want to deposit " .. money .. " gold coins into your bank account?"
    end
    return false, "I'm sorry, but you can't deposit a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if player:getMoney() < money then
        return false, "You don't have enough money to deposit " .. money .. " gold."
    end
    player:depositMoney(money)
    handler:resetData(player)
    return true, "You have deposited " .. money .. " gold coins into your bank account."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Bank Withdraw
local withdraw = greet:keyword("withdraw")
withdraw:respond("How much money would you like to withdraw?")
local answer = withdraw:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getBankBalance() < money then
            return false, "You don't have enough money to withdraw " .. money .. " gold."
        end
        if not player:canCarryMoney(money) then
            return false, "You can't carry that much money."
        end
        handler:addData(player, "money", money)
        return true, "You want to withdraw " .. money .. " gold coins from your bank account?"
    end
    return false, "I'm sorry, but you can't withdraw a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    player:withdrawMoney(money)
    handler:resetData(player)
    return true, "You have withdrawn " .. money .. " gold coins from your bank account."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Bank Transfer
local transfer = greet:keyword("transfer")
transfer:respond("You want to transfer money to another player? Please tell me the amount and the name of the player.")
local answer = transfer:onAnswer()
function answer:callback(npc, player, message, handler)
    local data = string.split(message, " ")
    local money = 0
    local playerName = ""
    for i = 1, #data do
        if tonumber(data[i]) then
            money = tonumber(data[i])
        else
            playerName = playerName ~= "" and playerName .." ".. data[i] or data[i]
        end
    end
    local receiver = getPlayerDatabaseInfo(playerName)
    if not receiver then
        return false, "There is no one named like ".. playerName
    end
    if receiver.vocation == VOCATION_NONE or player:getVocation() == VOCATION_NONE then
        return false, "You can't transfer money to or from a player without a vocation."
    end
    if receiver.name == player:getName() then
        return false, "You can't transfer money to yourself."
    end
    if not isValidMoney(money) then
        return false, "You can't transfer a negative amount of money or no money at all."
    end
    if player:getBankBalance() < money then
        return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
    end
    handler:addData(player, "money", money)
    handler:addData(player, "playerName", playerName)
    return true, "You want to transfer money to ".. playerName .." in the amount of ".. money .." gold coins?"
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    local playerName = handler:getData(player, "playerName")
    local receiver = getPlayerDatabaseInfo(playerName)
    if not player:transferMoneyTo(receiver, money) then
        return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
    end
    handler:resetData(player)
    return true, "You have transferred ".. money .." gold coins to ".. playerName
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Bank Change
local change = greet:keyword("change")
change:respond("Would you like to change your coins? You can change {gold} coins into {platinum} coins, {platinum} coins into {gold} coins, {platinum} coins into {crystal} coins, or {crystal} coins into {platinum} coins.")
-- Change Gold to Platinum
local gold = change:keyword("gold")
gold:respond("How many platinum coins would you like to get?")
local answer = gold:onAnswer()
function answer:callback(npc, player, message, handler)
    print(message)
    local money = tonumber(message) * 100
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_GOLD_COIN) < money then
            return false, "You don't have enough money to change " .. money .. " gold coins, into ".. message .." platinum coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money .. " gold coins, into ".. message .." platinum coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_GOLD_COIN, money) then
        return false, "You don't have enough money to change " .. money .. " gold coins, into ".. math.floor(money / 100) .." platinum coins."
    end
    player:addItem(ITEM_PLATINUM_COIN, math.floor(money / 100))
    handler:resetData(player)
    return true, "You have changed " .. money .. " gold coins, into ".. math.floor(money / 100) .." platinum coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Change Platinum to Gold
local platinum = change:keyword("platinum")
platinum:respond("Would you like to change your platinum coins into {gold} coins? or would you like to change them into {crystal} coins?")
local gold = platinum:keyword("gold")
gold:respond("How many gold coins would you like to get?")
local answer = gold:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_PLATINUM_COIN) * 100 < money then
            return false, "You don't have enough platinum coins to change " .. money .. " gold coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money / 100 .. " platinum coins, into ".. money .." gold coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_PLATINUM_COIN, math.floor(money / 100)) then
        return false, "You don't have enough platinum coins to change " .. money / 100 .. " platinum coins, into ".. money .." gold coins."
    end
    player:addItem(ITEM_GOLD_COIN, money)
    handler:resetData(player)
    return true, "You have changed " .. money / 100 .. " platinum coins, into ".. money .." gold coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Change Platinum to Crystal
local crystal = platinum:keyword("crystal")
crystal:respond("How many crystal coins would you like to get?")
local answer = crystal:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_PLATINUM_COIN) * 100 < money * 10000 then
            return false, "You don't have enough platinum coins to change " .. money .. " crystal coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money * 100 .. " platinum coins, into ".. money .." crystal coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_PLATINUM_COIN, math.floor(money * 100)) then
        return false, "You don't have enough platinum coins to change " .. money * 100 .. " platinum coins, into ".. money .." crystal coins."
    end
    player:addItem(ITEM_CRYSTAL_COIN, money)
    handler:resetData(player)
    return true, "You have changed " .. money * 100 .. " platinum coins, into ".. money .." crystal coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Change Crystal to Platinum
local crystal = change:keyword("crystal")
crystal:respond("How many platinum coins would you like to get?")
local answer = crystal:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_CRYSTAL_COIN) * 10000 < money * 100 then
            return false, "You don't have enough crystal coins to change into " .. money .. " platinum coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money / 100 .. " crystal coins, into ".. money .." platinum coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_CRYSTAL_COIN, money) then
        return false, "You don't have enough crystal coins to change " .. money / 100 .. " crystal coins, into ".. money .." platinum coins."
    end
    player:addItem(ITEM_PLATINUM_COIN, money * 100)
    handler:resetData(player)
    return true, "You have changed " .. money / 100 .. " crystal coins, into ".. money .." platinum coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Fast transfer / deposit / withdraw
local fast = greet:onAnswer()
function fast:callback(npc, player, message, handler)
    local transfer = string.find(message, "transfer")
    if transfer then
        local msg = string.gsub(message, "transfer ", "")
        local data = string.split(msg, " ")
        local money = 0
        local playerName = ""
        for i = 1, #data do
            if tonumber(data[i]) then
                money = tonumber(data[i])
            else
                playerName = playerName ~= "" and playerName .." ".. data[i] or data[i]
            end
        end
        local receiver = getPlayerDatabaseInfo(playerName)
        if not receiver then
            return false, "There is no one named like '".. playerName .."'"
        end
        if receiver.name == player:getName() then
            return false, "You can't transfer money to yourself."
        end
        if receiver.vocation == VOCATION_NONE or player:getVocation() == VOCATION_NONE then
            return false, "You can't transfer money to or from a player without a vocation."
        end
        if not isValidMoney(money) then
            return false, "You can't transfer a negative amount of money or no money at all."
        end
        if player:getBankBalance() < money then
            return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
        end
        handler:addData(player, "money", money)
        handler:addData(player, "playerName", playerName)
        handler:addData(player, "type", "transfer")
        return true, "You want to transfer money to ".. playerName .." in the amount of ".. money .." gold coins?"
    end
    local deposit = string.find(message, "deposit")
    if deposit then
        local sub = string.gsub(message, "deposit ", "")
        local money = tonumber(sub)
        local valid = isValidMoney(money)
        if valid then
            if player:getMoney() < money then
                return false, "You don't have enough money to deposit " .. money .. " gold."
            end
            handler:addData(player, "money", money)
            handler:addData(player, "type", "deposit")
            return true, "You want to deposit " .. money .. " gold coins into your bank account?"
        end
        return false, "I'm sorry, but you can't deposit a negative amount of money or no money at all."
    end
    local withdraw = string.find(message, "withdraw")
    if withdraw then
        local sub = string.gsub(message, "withdraw ", "")
        local money = tonumber(sub)
        local valid = isValidMoney(money)
        if valid then
            if player:getBankBalance() < money then
                return false, "You don't have enough money to withdraw " .. money .. " gold."
            end
            if not player:canCarryMoney(money) then
                return false, "You can't carry that much money."
            end
            handler:addData(player, "money", money)
            handler:addData(player, "type", "withdraw")
            return true, "You want to withdraw " .. money .. " gold coins from your bank account?"
        end
        return false, "I'm sorry, but you can't withdraw a negative amount of money or no money at all."
    end
end
local accept = fast:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    local playerName = handler:getData(player, "playerName")
    local receiver = getPlayerDatabaseInfo(playerName)
    local type = handler:getData(player, "type")
    if type == "transfer" then
        if not player:transferMoneyTo(receiver, money) then
            return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
        end
        handler:resetData(player)
        return true, "You have transferred ".. money .." gold coins to ".. playerName
    elseif type == "deposit" then
        if player:getMoney() < money then
            return false, "You don't have enough money to deposit " .. money .. " gold."
        end
        player:depositMoney(money)
        handler:resetData(player)
        return true, "You have deposited " .. money .. " gold coins into your bank account."
    elseif type == "withdraw" then
        player:withdrawMoney(money)
        handler:resetData(player)
        return true, "You have withdrawn " .. money .. " gold coins from your bank account."
    end
    return false, "Something went wrong, please try again."
end
local decline = fast:keyword({"no"})
decline:respond("Ok then, not.")
--[[
    npc tree sturcture looks like this now:
    greet
        |- help
        |- bank account
        |   |- basic
        |   |- advanced
        |       |- rent
        |       |- market
        |- balance
        |- deposit
        |   |- answer
        |       |- yes
        |       |- no
        |- withdraw
        |   |- answer
        |       |- yes
        |       |- no
        |- transfer
        |   |- answer
        |       |- yes
        |       |- no
        |- change
        |   |- gold
        |   |   |- answer
        |   |       |- yes
        |   |       |- no
        |   |- platinum
        |   |   |- gold
        |   |   |   |- answer
        |   |   |       |- yes
        |   |   |       |- no
        |   |   |- crystal
        |   |       |- answer
        |   |           |- yes
        |   |           |- no
        |   |- crystal
        |       |- platinum
        |           |- answer
        |               |- yes
        |               |- no
        |- fast (transfer, deposit, withdraw)
            |- yes
            |- no
]]
This is already a really advanced NPC including several different functions involved, if needed I'll make an extra post and make some more detailed explanations on how different parts work but I didn't wanted to clusterfuck this one with lots of commentary
Post automatically merged:

This actually looks like an amazing system.

Is there a guide on how to add this npc system to the server?

I would then do it and start making some nice npc’s

Had a quick look.

Is this the only commit needed to do?

Yea those are the only changes required, however I'm not sure how easy it would be to add this to a rather older source code.
For those interested in adding this to Canary it should actually be really easy with just a few changes, as Canary already have lua based NPC's it's just a matter of tweaking some lua functions probably

How about some callbacks?
Ex. shop2:addDiscount(9998) as callback:
  • returning float with price multiplier, not discount, so 10% discount is 0.9 of price (storage set to 90 = 90% of price)
  • make it able to apply multiple callbacks one after another
  • make it able to increase price, not only decrease
  • takes itemId as callback parameter, make it able to reduce/increase price of specific item
  • [optional feature] callback may return second parameter true / false ex. return 0.8, true, where second parameter would be stop, which would tell NPC system, if it should apply next price callbacks or stop (missing second parameter = nil -> false)
  • [optional feature] addPriceCallback may accept second parameter with callback priority, to make callbacks execute in some order
Lua:
shop2:addPriceCallback(function(player, itemId)
    if player:getStorage(9998) > 0 then
        return player:getStorage(9998) / 100
    end

    return 1.0
end)
Ex. with callback that reduces price by 20% for Magic Sword (2400) for players with storage 1234 set to 1:
Lua:
shop2:addPriceCallback(function(player, itemId)
    if player:getStorage(1234) == 1 and itemId == 2400 then
        return 0.8
    end

    return 1.0
end)
I'll enhance the shop module to make stuff like that possible, maybe just enhance the table when adding items, including a discount table that way it's easy for people to implement/setup with the floating multiplier like you mentioned, will think about a good way to add this
 
Last edited:
1) I'll make an oracle npc soon good one actually :D
3) I have not added a module for that one but I'll write it down, it could be achieved with callbacks tho (will include an example later on how to do that)

2) Banker NPC
Lua:
local npc = Game.createNpcType("Banker")
npc:speechBubble(SPEECHBUBBLE_TRADE)
npc:outfit({lookType = 472, lookHead = 40, lookBody = 95, lookLegs = 114, lookFeet = 27, addons = 1})
npc:defaultBehavior()
-- The NpcsHandler class is used to handle the NPC's responses to player interactions.
local handler = NpcsHandler(npc)
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse("Welcome to the bank, |PLAYERNAME|! Need some help with your {bank account}?")
local help = greet:keyword("help")
help:respond("You can check the {balance} of your bank account, {deposit} money or {withdraw} it. You can {transfer} money to other characters, provided that they have a vocation, or {change} the coins in your inventory.")
local bankAccount = greet:keyword("bank account")
bankAccount:respond("Would you like to know more about the {basic} functions of your bank account, the {advanced} functions, or are you already bored, perhaps?")
local basic = bankAccount:keyword({"basic", "functions", "job"})
basic:respond("I work in this bank. I can {change} money for you and help you with your {bank account}.")
local advanced = bankAccount:keyword("advanced")
advanced:respond("Your bank account will be used automatically when you want to {rent} a house or place an offer on an item on the {market}. Let me know if you want to know about how either one works.")
local rent = advanced:keyword("rent")
rent:respond("Once you have acquired a house the rent will be charged automatically from your {bank account} every month.")
local market = advanced:keyword("market")
market:respond("If you buy an item from the market, the required gold will be deducted from your bank account automatically. On the other hand, money you earn for selling items via the market will be added to your account. It's easy!")
-- Bank Balance
local balance = greet:keyword("balance")
function balance:callback(npc, player, message, handler)
    local balance = player:getBankBalance()
    if balance >= 100000000 then
        return true, "I think you must be one of the richest inhabitants in the world! Your account balance is " .. balance .. " gold."
    elseif balance >= 10000000 then
        return true, "You have made ten millions and it still grows! Your account balance is " .. balance .. " gold."
    elseif balance >= 1000000 then
        return true, "Wow, you have reached the magic number of a million gp!!! Your account balance is " .. balance.. " gold!"
    elseif balance >= 100000 then
        return true, "You certainly have made a pretty penny. Your account balance is " .. balance .. " gold."
    end
    return true, "Your account balance is " .. balance .. " gold."
end
-- Bank Deposit
local deposit = greet:keyword("deposit")
deposit:respond("How much money would you like to deposit?")
local answer = deposit:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getMoney() < money then
            return false, "You don't have enough money to deposit " .. money .. " gold."
        end
        handler:addData(player, "money", money)
        return true, "You want to deposit " .. money .. " gold coins into your bank account?"
    end
    return false, "I'm sorry, but you can't deposit a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if player:getMoney() < money then
        return false, "You don't have enough money to deposit " .. money .. " gold."
    end
    player:depositMoney(money)
    handler:resetData(player)
    return true, "You have deposited " .. money .. " gold coins into your bank account."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Bank Withdraw
local withdraw = greet:keyword("withdraw")
withdraw:respond("How much money would you like to withdraw?")
local answer = withdraw:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getBankBalance() < money then
            return false, "You don't have enough money to withdraw " .. money .. " gold."
        end
        if not player:canCarryMoney(money) then
            return false, "You can't carry that much money."
        end
        handler:addData(player, "money", money)
        return true, "You want to withdraw " .. money .. " gold coins from your bank account?"
    end
    return false, "I'm sorry, but you can't withdraw a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    player:withdrawMoney(money)
    handler:resetData(player)
    return true, "You have withdrawn " .. money .. " gold coins from your bank account."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Bank Transfer
local transfer = greet:keyword("transfer")
transfer:respond("You want to transfer money to another player? Please tell me the amount and the name of the player.")
local answer = transfer:onAnswer()
function answer:callback(npc, player, message, handler)
    local data = string.split(message, " ")
    local money = 0
    local playerName = ""
    for i = 1, #data do
        if tonumber(data[i]) then
            money = tonumber(data[i])
        else
            playerName = playerName ~= "" and playerName .." ".. data[i] or data[i]
        end
    end
    local receiver = getPlayerDatabaseInfo(playerName)
    if not receiver then
        return false, "There is no one named like ".. playerName
    end
    if receiver.vocation == VOCATION_NONE or player:getVocation() == VOCATION_NONE then
        return false, "You can't transfer money to or from a player without a vocation."
    end
    if receiver.name == player:getName() then
        return false, "You can't transfer money to yourself."
    end
    if not isValidMoney(money) then
        return false, "You can't transfer a negative amount of money or no money at all."
    end
    if player:getBankBalance() < money then
        return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
    end
    handler:addData(player, "money", money)
    handler:addData(player, "playerName", playerName)
    return true, "You want to transfer money to ".. playerName .." in the amount of ".. money .." gold coins?"
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    local playerName = handler:getData(player, "playerName")
    local receiver = getPlayerDatabaseInfo(playerName)
    if not player:transferMoneyTo(receiver, money) then
        return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
    end
    handler:resetData(player)
    return true, "You have transferred ".. money .." gold coins to ".. playerName
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Bank Change
local change = greet:keyword("change")
change:respond("Would you like to change your coins? You can change {gold} coins into {platinum} coins, {platinum} coins into {gold} coins, {platinum} coins into {crystal} coins, or {crystal} coins into {platinum} coins.")
-- Change Gold to Platinum
local gold = change:keyword("gold")
gold:respond("How many platinum coins would you like to get?")
local answer = gold:onAnswer()
function answer:callback(npc, player, message, handler)
    print(message)
    local money = tonumber(message) * 100
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_GOLD_COIN) < money then
            return false, "You don't have enough money to change " .. money .. " gold coins, into ".. message .." platinum coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money .. " gold coins, into ".. message .." platinum coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_GOLD_COIN, money) then
        return false, "You don't have enough money to change " .. money .. " gold coins, into ".. math.floor(money / 100) .." platinum coins."
    end
    player:addItem(ITEM_PLATINUM_COIN, math.floor(money / 100))
    handler:resetData(player)
    return true, "You have changed " .. money .. " gold coins, into ".. math.floor(money / 100) .." platinum coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Change Platinum to Gold
local platinum = change:keyword("platinum")
platinum:respond("Would you like to change your platinum coins into {gold} coins? or would you like to change them into {crystal} coins?")
local gold = platinum:keyword("gold")
gold:respond("How many gold coins would you like to get?")
local answer = gold:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_PLATINUM_COIN) * 100 < money then
            return false, "You don't have enough platinum coins to change " .. money .. " gold coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money / 100 .. " platinum coins, into ".. money .." gold coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_PLATINUM_COIN, math.floor(money / 100)) then
        return false, "You don't have enough platinum coins to change " .. money / 100 .. " platinum coins, into ".. money .." gold coins."
    end
    player:addItem(ITEM_GOLD_COIN, money)
    handler:resetData(player)
    return true, "You have changed " .. money / 100 .. " platinum coins, into ".. money .." gold coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Change Platinum to Crystal
local crystal = platinum:keyword("crystal")
crystal:respond("How many crystal coins would you like to get?")
local answer = crystal:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_PLATINUM_COIN) * 100 < money * 10000 then
            return false, "You don't have enough platinum coins to change " .. money .. " crystal coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money * 100 .. " platinum coins, into ".. money .." crystal coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_PLATINUM_COIN, math.floor(money * 100)) then
        return false, "You don't have enough platinum coins to change " .. money * 100 .. " platinum coins, into ".. money .." crystal coins."
    end
    player:addItem(ITEM_CRYSTAL_COIN, money)
    handler:resetData(player)
    return true, "You have changed " .. money * 100 .. " platinum coins, into ".. money .." crystal coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Change Crystal to Platinum
local crystal = change:keyword("crystal")
crystal:respond("How many platinum coins would you like to get?")
local answer = crystal:onAnswer()
function answer:callback(npc, player, message, handler)
    local money = tonumber(message)
    local valid = isValidMoney(money)
    if valid then
        if player:getItemCount(ITEM_CRYSTAL_COIN) * 10000 < money * 100 then
            return false, "You don't have enough crystal coins to change into " .. money .. " platinum coins."
        end
        handler:addData(player, "money", money)
        return true, "You want to change " .. money / 100 .. " crystal coins, into ".. money .." platinum coins?"
    end
    return false, "I'm sorry, but you can't change a negative amount of money or no money at all."
end
local accept = answer:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    if not player:removeItem(ITEM_CRYSTAL_COIN, money) then
        return false, "You don't have enough crystal coins to change " .. money / 100 .. " crystal coins, into ".. money .." platinum coins."
    end
    player:addItem(ITEM_PLATINUM_COIN, money * 100)
    handler:resetData(player)
    return true, "You have changed " .. money / 100 .. " crystal coins, into ".. money .." platinum coins."
end
local decline = answer:keyword({"no"})
decline:respond("Ok then, not.")
-- Fast transfer / deposit / withdraw
local fast = greet:onAnswer()
function fast:callback(npc, player, message, handler)
    local transfer = string.find(message, "transfer")
    if transfer then
        local msg = string.gsub(message, "transfer ", "")
        local data = string.split(msg, " ")
        local money = 0
        local playerName = ""
        for i = 1, #data do
            if tonumber(data[i]) then
                money = tonumber(data[i])
            else
                playerName = playerName ~= "" and playerName .." ".. data[i] or data[i]
            end
        end
        local receiver = getPlayerDatabaseInfo(playerName)
        if not receiver then
            return false, "There is no one named like '".. playerName .."'"
        end
        if receiver.name == player:getName() then
            return false, "You can't transfer money to yourself."
        end
        if receiver.vocation == VOCATION_NONE or player:getVocation() == VOCATION_NONE then
            return false, "You can't transfer money to or from a player without a vocation."
        end
        if not isValidMoney(money) then
            return false, "You can't transfer a negative amount of money or no money at all."
        end
        if player:getBankBalance() < money then
            return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
        end
        handler:addData(player, "money", money)
        handler:addData(player, "playerName", playerName)
        handler:addData(player, "type", "transfer")
        return true, "You want to transfer money to ".. playerName .." in the amount of ".. money .." gold coins?"
    end
    local deposit = string.find(message, "deposit")
    if deposit then
        local sub = string.gsub(message, "deposit ", "")
        local money = tonumber(sub)
        local valid = isValidMoney(money)
        if valid then
            if player:getMoney() < money then
                return false, "You don't have enough money to deposit " .. money .. " gold."
            end
            handler:addData(player, "money", money)
            handler:addData(player, "type", "deposit")
            return true, "You want to deposit " .. money .. " gold coins into your bank account?"
        end
        return false, "I'm sorry, but you can't deposit a negative amount of money or no money at all."
    end
    local withdraw = string.find(message, "withdraw")
    if withdraw then
        local sub = string.gsub(message, "withdraw ", "")
        local money = tonumber(sub)
        local valid = isValidMoney(money)
        if valid then
            if player:getBankBalance() < money then
                return false, "You don't have enough money to withdraw " .. money .. " gold."
            end
            if not player:canCarryMoney(money) then
                return false, "You can't carry that much money."
            end
            handler:addData(player, "money", money)
            handler:addData(player, "type", "withdraw")
            return true, "You want to withdraw " .. money .. " gold coins from your bank account?"
        end
        return false, "I'm sorry, but you can't withdraw a negative amount of money or no money at all."
    end
end
local accept = fast:keyword({"yes"})
function accept:callback(npc, player, message, handler)
    local money = handler:getData(player, "money")
    local playerName = handler:getData(player, "playerName")
    local receiver = getPlayerDatabaseInfo(playerName)
    local type = handler:getData(player, "type")
    if type == "transfer" then
        if not player:transferMoneyTo(receiver, money) then
            return false, "You don't have enough money to transfer ".. money .." gold coins to ".. playerName
        end
        handler:resetData(player)
        return true, "You have transferred ".. money .." gold coins to ".. playerName
    elseif type == "deposit" then
        if player:getMoney() < money then
            return false, "You don't have enough money to deposit " .. money .. " gold."
        end
        player:depositMoney(money)
        handler:resetData(player)
        return true, "You have deposited " .. money .. " gold coins into your bank account."
    elseif type == "withdraw" then
        player:withdrawMoney(money)
        handler:resetData(player)
        return true, "You have withdrawn " .. money .. " gold coins from your bank account."
    end
    return false, "Something went wrong, please try again."
end
local decline = fast:keyword({"no"})
decline:respond("Ok then, not.")
--[[
    npc tree sturcture looks like this now:
    greet
        |- help
        |- bank account
        |   |- basic
        |   |- advanced
        |       |- rent
        |       |- market
        |- balance
        |- deposit
        |   |- answer
        |       |- yes
        |       |- no
        |- withdraw
        |   |- answer
        |       |- yes
        |       |- no
        |- transfer
        |   |- answer
        |       |- yes
        |       |- no
        |- change
        |   |- gold
        |   |   |- answer
        |   |       |- yes
        |   |       |- no
        |   |- platinum
        |   |   |- gold
        |   |   |   |- answer
        |   |   |       |- yes
        |   |   |       |- no
        |   |   |- crystal
        |   |       |- answer
        |   |           |- yes
        |   |           |- no
        |   |- crystal
        |       |- platinum
        |           |- answer
        |               |- yes
        |               |- no
        |- fast (transfer, deposit, withdraw)
            |- yes
            |- no
]]
This is already a really advanced NPC including several different functions involved, if needed I'll make an extra post and make some more detailed explanations on how different parts work but I didn't wanted to clusterfuck this one with lots of commentary
Post automatically merged:


Yea those are the only changes required, however I'm not sure how easy it would be to add this to a rather older source code.
For those interested in adding this to Canary it should actually be really easy with just a few changes, as Canary already have lua based NPC's it's just a matter of tweaking some lua functions probably


I'll enhance the shop module to make stuff like that possible, maybe just enhance the table when adding items, including a discount table that way it's easy for people to implement/setup with the floating multiplier like you mentioned, will think about a good way to add this
Alright thanks for the reply!! I am going to add it and see how it all goes. 💪🏼💪🏼💪🏼
 
1. How would the oracle looks like
Oracle:

This is a flexible script which only requires the table setups to be changed (towns, vocations) in order for everything to work, nothing more.
Lua:
local towns = {
    ["town1"] = Position(98, 106, 6),
    ["town2"] = Position(79, 99, 6),
    ["town3"] = Position(94, 129, 7)
}

local vocations = {
    ["knight"] = 1,
    ["paladin"] = 2,
    ["sorcerer"] = 3,
    ["druid"] = 4
}

local townStr = ""
for k,v in pairs(towns) do
    townStr = townStr == "" and "{".. k:upper() .."}" or townStr .. ", {".. k:upper() .."}"
end

local vocationStr = ""
for k,v in pairs(vocations) do
    vocationStr = vocationStr == "" and "{".. k:upper() .."}" or vocationStr .. ", {".. k:upper() .."}"
end

-- creating the new npc type with the name "Oracle"
local npcType = Game.createNpcType("Oracle")
npcType:speechBubble(3)
npcType:outfit({lookTypeEx = 1448})
npcType:spawnRadius(0)
npcType:walkInterval(0)
npcType:walkSpeed(0)
npcType:defaultBehavior()

local handler = NpcsHandler(npcType)
handler:setFarewellResponse("COME BACK WHEN YOU ARE PREPARED TO FACE YOUR DESTINY!")

local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse("|PLAYERNAME|, ARE YOU PREPARED TO FACE YOUR DESTINY?")
local prepared = greet:keyword("yes")

function prepared:callback(npc, player, message, handler)
    local voc = player:getVocation()
    if player:getLevel() < 8 then
        return false, "CHILD! COME BACK WHEN YOU HAVE GROWN UP!"
    elseif player:getLevel() > 9 then
        return false, "|PLAYERNAME|, I CAN'T LET YOU LEAVE - YOU ARE TOO STRONG ALREADY! YOU CAN ONLY LEAVE WITH LEVEL 9 OR LOWER."
    end
    if voc ~= VOCATION_NONE then
        return false, "YOU ALREADY HAVE A VOCATION!"
    end
    return true, "IN WHICH TOWN DO YOU WANT TO LIVE: ".. townStr .."?"
end

for townName, pos in pairs(towns) do
    -- creating a keyword for every town
    local town = prepared:keyword(townName)
    -- setting a response for every town
    town:respond("IN ".. townName:upper() .."! AND WHAT PROFESSION HAVE YOU CHOSEN: ".. vocationStr .."?")
    for vocation, id in pairs(vocations) do
        -- creating keywords for every vocation inside every town keyword
        local voc = town:keyword(vocation)
        voc:respond("A ".. vocation:upper() .."! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!")
       
        -- creating a keyword for every vocation to decline the choice
        local decline = voc:keyword("no")
        decline:respond("DONT WASTE MY TIME THEN!")
        -- instantly ungreets and looses focus
        decline:farewell()
       
        -- creating a keyword for every vocation to accept the choice
        local accept = voc:keyword("yes")
        -- setting the vocation and town for the player inside the callback
        function accept:callback(npc, player, message, handler)
            player:setVocation(Vocation(id))
            player:setTown(Town(townName))
            player:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
            pos:sendMagicEffect(CONST_ME_TELEPORT)
            return true
        end
        -- teleporting the player to the town position
        accept:teleport(pos)
        accept:respond("SO BE IT!")
    end
end
This is probably a good example on showing how flexible it can be.
 
Oracle:

This is a flexible script which only requires the table setups to be changed (towns, vocations) in order for everything to work, nothing more.
Lua:
local towns = {
    ["town1"] = Position(98, 106, 6),
    ["town2"] = Position(79, 99, 6),
    ["town3"] = Position(94, 129, 7)
}

local vocations = {
    ["knight"] = 1,
    ["paladin"] = 2,
    ["sorcerer"] = 3,
    ["druid"] = 4
}

local townStr = ""
for k,v in pairs(towns) do
    townStr = townStr == "" and "{".. k:upper() .."}" or townStr .. ", {".. k:upper() .."}"
end

local vocationStr = ""
for k,v in pairs(vocations) do
    vocationStr = vocationStr == "" and "{".. k:upper() .."}" or vocationStr .. ", {".. k:upper() .."}"
end

local npcType = Game.createNpcType("Oracle")
npcType:speechBubble(3)
npcType:outfit({lookTypeEx = 1448})
npcType:spawnRadius(0)
npcType:walkInterval(0)
npcType:walkSpeed(0)
npcType:defaultBehavior()

local handler = NpcsHandler(npcType)
handler:setFarewellResponse("COME BACK WHEN YOU ARE PREPARED TO FACE YOUR DESTINY!")

local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse("|PLAYERNAME|, ARE YOU PREPARED TO FACE YOUR DESTINY?")
local prepared = greet:keyword("yes")

function prepared:callback(npc, player, message, handler)
    local voc = player:getVocation()
    if player:getLevel() < 8 then
        return false, "CHILD! COME BACK WHEN YOU HAVE GROWN UP!"
    elseif player:getLevel() > 9 then
        return false, "|PLAYERNAME|, I CAN'T LET YOU LEAVE - YOU ARE TOO STRONG ALREADY! YOU CAN ONLY LEAVE WITH LEVEL 9 OR LOWER."
    end
    if voc ~= VOCATION_NONE then
        return false, "YOU ALREADY HAVE A VOCATION!"
    end
    return true, "IN WHICH TOWN DO YOU WANT TO LIVE: ".. townStr .."?"
end

for townName, pos in pairs(towns) do
    -- creating a keyword for every town
    local town = prepared:keyword(townName)
    -- setting a response for every town
    town:respond("IN ".. townName:upper() .."! AND WHAT PROFESSION HAVE YOU CHOSEN: ".. vocationStr .."?")
    for vocation, id in pairs(vocations) do
        -- creating keywords for every vocation inside every town keyword
        local voc = town:keyword(vocation)
        voc:respond("A ".. vocation:upper() .."! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!")
   
        -- creating a keyword for every vocation to decline the choice
        local decline = voc:keyword("no")
        decline:respond("DONT WASTE MY TIME THEN!")
        -- instantly ungreets and looses focus
        decline:farewell()
   
        -- creating a keyword for every vocation to accept the choice
        local accept = voc:keyword("yes")
        -- setting the vocation and town for the player inside the callback
        function accept:callback(npc, player, message, handler)
            player:setVocation(Vocation(id))
            player:setTown(Town(townName))
            player:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
            pos:sendMagicEffect(CONST_ME_TELEPORT)
            return true
        end
        -- teleporting the player to the town position
        accept:teleport(pos)
        accept:respond("SO BE IT!")
    end
end
This is probably a good example on showing how flexible it can be.

---

You could make it more simple by implementing helper functions, enhanced callback functions for logic improvements, and loops for dynamic interactions that provide an easier path for scalability. Though, I haven't written OT Lua in half a decade, so cut me some slack if I'm being an idiot here.

Lua:
local towns = {
    ["town1"] = Position(98, 106, 6),
    ["town2"] = Position(79, 99, 6),
    ["town3"] = Position(94, 129, 7)
}

local vocations = {
    ["knight"] = 1,
    ["paladin"] = 2,
    ["sorcerer"] = 3,
    ["druid"] = 4
}

local function createChoicesString(choices)
    local parts = {}
    for k in pairs(choices) do
        table.insert(parts, "{" .. k:upper() .. "}")
    end
    return table.concat(parts, ", ")
end

local townStr = createChoicesString(towns)
local vocationStr = createChoicesString(vocations)

-- Create the new NPC type with the name "Oracle"
local npcType = Game.createNpcType("Oracle")
npcType:speechBubble(3)
npcType:outfit({lookTypeEx = 1448})
npcType:spawnRadius(0)
npcType:walkInterval(0)
npcType:walkSpeed(0)
npcType:defaultBehavior()

local handler = NpcsHandler(npcType)
handler:setFarewellResponse("COME BACK WHEN YOU ARE PREPARED TO FACE YOUR DESTINY!")

local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse("|PLAYERNAME|, ARE YOU PREPARED TO FACE YOUR DESTINY?")

local prepared = greet:keyword("yes")

function prepared:callback(npc, player, message, handler)
    if player:getLevel() < 8 then
        return false, "CHILD! COME BACK WHEN YOU HAVE GROWN UP!"
    elseif player:getLevel() > 9 then
        return false, "|PLAYERNAME|, I CAN'T LET YOU LEAVE - YOU ARE TOO STRONG ALREADY! YOU CAN ONLY LEAVE WITH LEVEL 9 OR LOWER."
    elseif player:getVocation() ~= VOCATION_NONE then
        return false, "YOU ALREADY HAVE A VOCATION!"
    end
    return true, "IN WHICH TOWN DO YOU WANT TO LIVE: " .. townStr .. "?"
end

local function createTownVocationKeywords(townName, pos, handler)
    local town = prepared:keyword(townName)
    town:respond("IN " .. townName:upper() .. "! AND WHAT PROFESSION HAVE YOU CHOSEN: " .. vocationStr .. "?")

    for vocation, id in pairs(vocations) do
        local voc = town:keyword(vocation)
        voc:respond("A " .. vocation:upper() .. "! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!")

        local decline = voc:keyword("no")
        decline:respond("DON'T WASTE MY TIME THEN!")
        decline:farewell()

        local accept = voc:keyword("yes")
        function accept:callback(npc, player, message, handler)
            player:setVocation(Vocation(id))
            player:setTown(Town(townName))
            player:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
            pos:sendMagicEffect(CONST_ME_TELEPORT)
            return true
        end
        accept:teleport(pos)
        accept:respond("SO BE IT!")
    end
end

for townName, pos in pairs(towns) do
    createTownVocationKeywords(townName, pos, handler)
end

npcHandler:addModule(FocusModule:new())
 
---

You could make it more simple by implementing helper functions, enhanced callback functions for logic improvements, and loops for dynamic interactions that provide an easier path for scalability. Though, I haven't written OT Lua in half a decade, so cut me some slack if I'm being an idiot here.

Lua:
local towns = {
    ["town1"] = Position(98, 106, 6),
    ["town2"] = Position(79, 99, 6),
    ["town3"] = Position(94, 129, 7)
}

local vocations = {
    ["knight"] = 1,
    ["paladin"] = 2,
    ["sorcerer"] = 3,
    ["druid"] = 4
}

local function createChoicesString(choices)
    local parts = {}
    for k in pairs(choices) do
        table.insert(parts, "{" .. k:upper() .. "}")
    end
    return table.concat(parts, ", ")
end

local townStr = createChoicesString(towns)
local vocationStr = createChoicesString(vocations)

-- Create the new NPC type with the name "Oracle"
local npcType = Game.createNpcType("Oracle")
npcType:speechBubble(3)
npcType:outfit({lookTypeEx = 1448})
npcType:spawnRadius(0)
npcType:walkInterval(0)
npcType:walkSpeed(0)
npcType:defaultBehavior()

local handler = NpcsHandler(npcType)
handler:setFarewellResponse("COME BACK WHEN YOU ARE PREPARED TO FACE YOUR DESTINY!")

local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse("|PLAYERNAME|, ARE YOU PREPARED TO FACE YOUR DESTINY?")

local prepared = greet:keyword("yes")

function prepared:callback(npc, player, message, handler)
    if player:getLevel() < 8 then
        return false, "CHILD! COME BACK WHEN YOU HAVE GROWN UP!"
    elseif player:getLevel() > 9 then
        return false, "|PLAYERNAME|, I CAN'T LET YOU LEAVE - YOU ARE TOO STRONG ALREADY! YOU CAN ONLY LEAVE WITH LEVEL 9 OR LOWER."
    elseif player:getVocation() ~= VOCATION_NONE then
        return false, "YOU ALREADY HAVE A VOCATION!"
    end
    return true, "IN WHICH TOWN DO YOU WANT TO LIVE: " .. townStr .. "?"
end

local function createTownVocationKeywords(townName, pos, handler)
    local town = prepared:keyword(townName)
    town:respond("IN " .. townName:upper() .. "! AND WHAT PROFESSION HAVE YOU CHOSEN: " .. vocationStr .. "?")

    for vocation, id in pairs(vocations) do
        local voc = town:keyword(vocation)
        voc:respond("A " .. vocation:upper() .. "! ARE YOU SURE? THIS DECISION IS IRREVERSIBLE!")

        local decline = voc:keyword("no")
        decline:respond("DON'T WASTE MY TIME THEN!")
        decline:farewell()

        local accept = voc:keyword("yes")
        function accept:callback(npc, player, message, handler)
            player:setVocation(Vocation(id))
            player:setTown(Town(townName))
            player:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
            pos:sendMagicEffect(CONST_ME_TELEPORT)
            return true
        end
        accept:teleport(pos)
        accept:respond("SO BE IT!")
    end
end

for townName, pos in pairs(towns) do
    createTownVocationKeywords(townName, pos, handler)
end

npcHandler:addModule(FocusModule:new())
I just quickly wrote that entire thing just for a showcase, you can always improve something here and there :D
A lot of stuff you are talking about is already created, the examples I provided so far where all very custom, that's why most of the generic helpers wont work for them
What you basicly did with wraping everything inside a function, is what I do at modules (travelTo as an example)
I plan to add a lot more of them later on or even let the community create them obviously
 

I am loving this system!!! so easy and so good!!

Here is the npc for you guys! its amazing

Lua:
local npcType = Game.createNpcType("Storyteller")
npcType:outfit({lookType = 128, lookHead = 78, lookBody = 76, lookLegs = 78, lookFeet = 76})
npcType:spawnRadius(2)
npcType:walkInterval(5000)
npcType:walkSpeed(80)
npcType:defaultBehavior()

local handler = NpcsHandler(npcType)
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse("Hello |PLAYERNAME|! Would you like to hear a {story}?")

local story = greet:keyword("story")

function story:callback(npc, player, message, handler)
    local talk = NpcTalkQueue(npc)
    local messages = {
        "This is part 1...",
        "And here we have part 2.",
        "What if we made this part 3.",
        "Then this will be 4 after 3.",
        "And the last one is 5."
    }
    talk:addToQueue(player, "Sit back and enjoy the story.", 0)
    for i, message in ipairs(messages) do
        talk:addToQueue(player, message, i * 2000)
    end
    return true
end

local decline = greet:keyword("no")
decline:respond("Alright then, maybe next time.")

Add decline:farewell() after the decline respond to make the npc lose focus immediately as an extra rpg aspect :p
 
Last edited:
A quick question: is it possible to run two types of NPCs simultaneously, one based on LUA and the other a standard NPC with XML and a script? If I update this commit to my TFS, will I lose all the NPCs and have to rewrite all the NPCs to be LUA-based?
 
A quick question: is it possible to run two types of NPCs simultaneously, one based on LUA and the other a standard NPC with XML and a script? If I update this commit to my TFS, will I lose all the NPCs and have to rewrite all the NPCs to be LUA-based?
You can run both at the same time, there is no plan on deprecating xml npcs before 1.8 release, that way people have enough time to adapt and re write their npcs into the new system before we remove the old one.
 
Now we have to figure out how to make them talk to one another and be set on a schedule where they do things / go places at a particular time of day. I feel like it's possible, but I don't know how to start.

A good place to start would be "onThink" "getWorldHour" and "addEvent".

I guess you could make it via a scheduled event but would be cool if they were able to "listen" for responses / text like they do for players. Is that possible?

Something like:

Lua:
local npc1 = {}

function onThink()
    if os.time() % 120 == 0 then -- Every 2 minutes
        local currentHour = getWorldHour()
        doCreatureSay(getNpcId(), "Hello NPC2, do you know what time it is?", TALKTYPE_SAY)
        addEvent(sendMessageToNPC2, 2000, "The current game time is " .. currentHour .. " o'clock.")
    end
end

function sendMessageToNPC2(message)
    local npc2 = getNpcByName("NPC2")
    if npc2 then
        doCreatureSay(npc2, message, TALKTYPE_SAY)
    end
end

function onCreatureAppear(creature)
end

function onCreatureDisappear(creature)
end

function onCreatureSay(creature, type, message)
end

function onThink()
    -- Your regular think logic
end

function onIdle()
    -- Your regular idle logic
end

npc1.onThink = onThink
npc1.onCreatureAppear = onCreatureAppear
npc1.onCreatureDisappear = onCreatureDisappear
npc1.onCreatureSay = onCreatureSay
npc1.onIdle = onIdle

registerNpc(npc1)

and:

Lua:
local npc2 = {}

function onThink()
    -- Regular think logic for NPC2
end

function onCreatureAppear(creature)
end

function onCreatureDisappear(creature)
end

function onCreatureSay(creature, type, message)
    if creature == getNpcByName("NPC1") and message == "Hello NPC2, do you know what time it is?" then
        local currentHour = getWorldHour()
        addEvent(replyToNPC1, 2000, "The current game time is " .. currentHour .. " o'clock.")
    end
end

function replyToNPC1(message)
    doCreatureSay(getNpcId(), message, TALKTYPE_SAY)
end

npc2.onThink = onThink
npc2.onCreatureAppear = onCreatureAppear
npc2.onCreatureDisappear = onCreatureDisappear
npc2.onCreatureSay = onCreatureSay

registerNpc(npc2)

as a rough asf example...
 
Last edited:
Now we have to figure out how to make them talk to one another and be set on a schedule where they do things / go places at a particular time of day. I feel like it's possible, but I don't know how to start.

A good place to start would be "onThink" "getWorldHour" and "addEvent".

I guess you could make it via a scheduled event but would be cool if they were able to "listen" for responses / text like they do for players. Is that possible?

Something like:

Lua:
local npc1 = {}

function onThink()
    if os.time() % 120 == 0 then -- Every 2 minutes
        local currentHour = getWorldHour()
        doCreatureSay(getNpcId(), "Hello NPC2, do you know what time it is?", TALKTYPE_SAY)
        addEvent(sendMessageToNPC2, 2000, "The current game time is " .. currentHour .. " o'clock.")
    end
end

function sendMessageToNPC2(message)
    local npc2 = getNpcByName("NPC2")
    if npc2 then
        doCreatureSay(npc2, message, TALKTYPE_SAY)
    end
end

function onCreatureAppear(creature)
end

function onCreatureDisappear(creature)
end

function onCreatureSay(creature, type, message)
end

function onThink()
    -- Your regular think logic
end

function onIdle()
    -- Your regular idle logic
end

npc1.onThink = onThink
npc1.onCreatureAppear = onCreatureAppear
npc1.onCreatureDisappear = onCreatureDisappear
npc1.onCreatureSay = onCreatureSay
npc1.onIdle = onIdle

registerNpc(npc1)

and:

Lua:
local npc2 = {}

function onThink()
    -- Regular think logic for NPC2
end

function onCreatureAppear(creature)
end

function onCreatureDisappear(creature)
end

function onCreatureSay(creature, type, message)
    if creature == getNpcByName("NPC1") and message == "Hello NPC2, do you know what time it is?" then
        local currentHour = getWorldHour()
        addEvent(replyToNPC1, 2000, "The current game time is " .. currentHour .. " o'clock.")
    end
end

function replyToNPC1(message)
    doCreatureSay(getNpcId(), message, TALKTYPE_SAY)
end

npc2.onThink = onThink
npc2.onCreatureAppear = onCreatureAppear
npc2.onCreatureDisappear = onCreatureDisappear
npc2.onCreatureSay = onCreatureSay

registerNpc(npc2)

as a rough asf example...
This is actually a very good idea….
 
Now we have to figure out how to make them talk to one another and be set on a schedule where they do things / go places at a particular time of day. I feel like it's possible, but I don't know how to start.

A good place to start would be "onThink" "getWorldHour" and "addEvent".

I guess you could make it via a scheduled event but would be cool if they were able to "listen" for responses / text like they do for players. Is that possible?

Something like:

Lua:
local npc1 = {}

function onThink()
    if os.time() % 120 == 0 then -- Every 2 minutes
        local currentHour = getWorldHour()
        doCreatureSay(getNpcId(), "Hello NPC2, do you know what time it is?", TALKTYPE_SAY)
        addEvent(sendMessageToNPC2, 2000, "The current game time is " .. currentHour .. " o'clock.")
    end
end

function sendMessageToNPC2(message)
    local npc2 = getNpcByName("NPC2")
    if npc2 then
        doCreatureSay(npc2, message, TALKTYPE_SAY)
    end
end

function onCreatureAppear(creature)
end

function onCreatureDisappear(creature)
end

function onCreatureSay(creature, type, message)
end

function onThink()
    -- Your regular think logic
end

function onIdle()
    -- Your regular idle logic
end

npc1.onThink = onThink
npc1.onCreatureAppear = onCreatureAppear
npc1.onCreatureDisappear = onCreatureDisappear
npc1.onCreatureSay = onCreatureSay
npc1.onIdle = onIdle

registerNpc(npc1)

and:

Lua:
local npc2 = {}

function onThink()
    -- Regular think logic for NPC2
end

function onCreatureAppear(creature)
end

function onCreatureDisappear(creature)
end

function onCreatureSay(creature, type, message)
    if creature == getNpcByName("NPC1") and message == "Hello NPC2, do you know what time it is?" then
        local currentHour = getWorldHour()
        addEvent(replyToNPC1, 2000, "The current game time is " .. currentHour .. " o'clock.")
    end
end

function replyToNPC1(message)
    doCreatureSay(getNpcId(), message, TALKTYPE_SAY)
end

npc2.onThink = onThink
npc2.onCreatureAppear = onCreatureAppear
npc2.onCreatureDisappear = onCreatureDisappear
npc2.onCreatureSay = onCreatureSay

registerNpc(npc2)

as a rough asf example...
Well you could achieve most of that by using callbacks
Lua:
local npcType = Game.createNpcType("example")
npcType:defaultBehavior()

function npcType:onThinkCallback()
-- your custom thinking code which does not override the default behavior of the npc
end

function npcType:onSayCallback(npc, creature, messageType, message)
-- your custom code which gets executed after the default onSay function was executed
end
You could utilize the NpcVoices class to let npc's talk to eachother, reacting to certain keywords
but this should actually all be custom user code or done as a separate module, since the scale of this will end up bloating the entire thinking process of every npc to an extent where this will get a bottleneck if it's executed by each npc thinking all the time (when he doesn't necessarily utilize it)
 
Well you could achieve most of that by using callbacks
Lua:
local npcType = Game.createNpcType("example")
npcType:defaultBehavior()

function npcType:onThinkCallback()
-- your custom thinking code which does not override the default behavior of the npc
end

function npcType:onSayCallback(npc, creature, messageType, message)
-- your custom code which gets executed after the default onSay function was executed
end
You could utilize the NpcVoices class to let npc's talk to eachother, reacting to certain keywords
but this should actually all be custom user code or done as a separate module, since the scale of this will end up bloating the entire thinking process of every npc to an extent where this will get a bottleneck if it's executed by each npc thinking all the time (when he doesn't necessarily utilize it)

Interesting, so something like this could be an example:


Lua:
local npc1Type = Game.createNpcType("NPC1")
npc1Type:defaultBehavior()

function npc1Type:onThinkCallback()
    -- Custom thinking logic for NPC1
    -- Example: NPC1 initiates conversation about the time every 2 minutes
    if os.time() % 120 == 0 then -- Every 2 minutes
        self:say("Hello NPC2, do you know what time it is?")
    end
 
    -- Additional custom logic for NPC1 if needed
    -- Example: NPC1 moves to a random position every 3 minutes
    if os.time() % 180 == 0 then
        local currentPos = self:getPosition()
        local newPos = {
            x = currentPos.x + math.random(-1, 1),
            y = currentPos.y + math.random(-1, 1),
            z = currentPos.z
        }
        self:moveTo(newPos)
    end
end

function npc1Type:onSayCallback(npc, creature, messageType, message)
    if creature:getName() == "NPC2" and message:find("The current game time is") then
        self:say("Thank you, NPC2!")
    end
end

npc1Type:register()


Lua:
local npc2Type = Game.createNpcType("NPC2")
npc2Type:defaultBehavior()

function npc2Type:onThinkCallback()
    -- Custom thinking logic for NPC2
    -- Example: NPC2 moves to a random position every minute
    if os.time() % 60 == 0 then
        local currentPos = self:getPosition()
        local newPos = {
            x = currentPos.x + math.random(-1, 1),
            y = currentPos.y + math.random(-1, 1),
            z = currentPos.z
        }
        self:moveTo(newPos)
    end
end

function npc2Type:onSayCallback(npc, creature, messageType, message)
    if creature:getName() == "NPC1" and message:find("do you know what time it is?") then
        local currentHour = getWorldHour()
        self:say("The current game time is " .. currentHour .. " o'clock.")
    end
end

npc2Type:register()

I guess they'd inevitably move further away from one another, so the interaction would become impossible. You could keep them in the same room, but that's kind of boring. Would be neat if you could make them stick close together or stay within boundaries, so you're able to place them anywhere on the map and still interact.

I wonder if this would work:


Lua:
local npc1Type = Game.createNpcType("NPC1")
npc1Type:defaultBehavior()

function npc1Type:onThinkCallback()
    -- Custom thinking logic for NPC1
    -- Example: NPC1 initiates conversation about the time every 2 minutes
    if os.time() % 120 == 0 then -- Every 2 minutes
        self:say("Hello NPC2, do you know what time it is?")
    end

    -- Ensure NPC1 stays close to NPC2 or moves randomly if they are close enough
    local npc2 = getNpcByName("NPC2")
    if npc2 then
        local distance = getDistanceBetween(self:getPosition(), npc2:getPosition())
        if distance > 3 then -- If the distance is greater than 3 tiles, move closer
            local direction = getDirectionTo(self:getPosition(), npc2:getPosition())
            self:move(direction)
        else
            -- Move randomly
            if os.time() % 60 == 0 then -- Every minute
                local currentPos = self:getPosition()
                local newPos = {
                    x = currentPos.x + math.random(-1, 1),
                    y = currentPos.y + math.random(-1, 1),
                    z = currentPos.z
                }
                self:moveTo(newPos)
            end
        end
    end
end

function npc1Type:onSayCallback(npc, creature, messageType, message)
    if creature:getName() == "NPC2" and message:find("The current game time is") then
        self:say("Thank you, NPC2!")
    end
end

npc1Type:register()


Lua:
local npc2Type = Game.createNpcType("NPC2")
npc2Type:defaultBehavior()

function npc2Type:onThinkCallback()
    -- Custom thinking logic for NPC2
    -- Example: NPC2 moves to a random position every minute
    if os.time() % 60 == 0 then
        local npc1 = getNpcByName("NPC1")
        local currentPos = self:getPosition()
        local newPos

        -- Ensure NPC2 stays close to NPC1
        if npc1 then
            local distance = getDistanceBetween(currentPos, npc1:getPosition())
            if distance > 3 then -- If the distance is greater than 3 tiles, move closer
                local direction = getDirectionTo(currentPos, npc1:getPosition())
                newPos = {
                    x = currentPos.x + direction.x,
                    y = currentPos.y + direction.y,
                    z = currentPos.z
                }
            else
                newPos = {
                    x = currentPos.x + math.random(-1, 1),
                    y = currentPos.y + math.random(-1, 1),
                    z = currentPos.z
                }
            end
        else
            newPos = {
                x = currentPos.x + math.random(-1, 1),
                y = currentPos.y + math.random(-1, 1),
                z = currentPos.z
            }
        end

        self:moveTo(newPos)
    end
end

function npc2Type:onSayCallback(npc, creature, messageType, message)
    if creature:getName() == "NPC1" and message:find("do you know what time it is?") then
        local currentHour = getWorldHour()
        self:say("The current game time is " .. currentHour .. " o'clock.")
    end
end

npc2Type:register()

That way there's a few things to stop that from happening:
1. Distance checking of the NPCs with 'getDistanceBetween'
2. Direction calculation to calculate the direction needed to reach the other npc with 'getDirectionTo'
3. They move randomly if 3 spaces or less apart
4. NPC1 moves randomly every minute, while NPC2 moves randomly every minute or closer to NPC1.
--- I feel that makes it a little more realistic. NPC1 asks for the time, NPC2 notices, so they either tell them then or move closer if they're too far away so NPC1 can "hear" them.
 
Last edited:
I can understand that the need for better AI is absolutely mandatory and I'll surely try to get more and more covered over the time.
Feel free to open up an Issue at Issues · otland/forgottenserver (https://github.com/otland/forgottenserver/issues) proposing your changes, that way I wont forget easily about them.
If no one requests any Npcs to be converted then I'll just post default datapack Npcs from time to time, we don't want this to get stale.
 
Last edited:
How about some callbacks?
Ex. shop2:addDiscount(9998) as callback:
  • returning float with price multiplier, not discount, so 10% discount is 0.9 of price (storage set to 90 = 90% of price)
  • make it able to apply multiple callbacks one after another
  • make it able to increase price, not only decrease
  • takes itemId as callback parameter, make it able to reduce/increase price of specific item
  • [optional feature] callback may return second parameter true / false ex. return 0.8, true, where second parameter would be stop, which would tell NPC system, if it should apply next price callbacks or stop (missing second parameter = nil -> false)
  • [optional feature] addPriceCallback may accept second parameter with callback priority, to make callbacks execute in some order
Lua:
shop2:addPriceCallback(function(player, itemId)
    if player:getStorage(9998) > 0 then
        return player:getStorage(9998) / 100
    end

    return 1.0
end)
Ex. with callback that reduces price by 20% for Magic Sword (2400) for players with storage 1234 set to 1:
Lua:
shop2:addPriceCallback(function(player, itemId)
    if player:getStorage(1234) == 1 and itemId == 2400 then
        return 0.8
    end

    return 1.0
end)
I've added something similar to your suggestion now
Lua:
local weaponShop = {
    [2400] = {buy = 2000, sell = 1000},
    [2402] = {buy = 50, sell = 25}
}

local shop1 = NpcShop(npc, 1)
shop1:addItems(weaponShop)
-- adds discount before calling callback, discount if set is always applied before
shop1:addDiscount(9999)
-- items (table) = before all discount (original price)
-- afterDiscount (table) (returns nil if no discount) = items with discounted price
function shop1:callback(npc, player, handler, items, afterDiscount)
    items = afterDiscount or items
    for k, v in pairs(items) do
        -- doubling the buy price for each item
        v.buy = v.buy * 2
    end
   -- need to return the items table
    return items
end
You would even be able to add new items in the callback function just by inserting them into table
 
I've added something similar to your suggestion now
Lua:
local weaponShop = {
    [2400] = {buy = 2000, sell = 1000},
    [2402] = {buy = 50, sell = 25}
}

local shop1 = NpcShop(npc, 1)
shop1:addItems(weaponShop)
-- adds discount before calling callback, discount if set is always applied before
shop1:addDiscount(9999)
-- items (table) = before all discount (original price)
-- afterDiscount (table) (returns nil if no discount) = items with discounted price
function shop1:callback(npc, player, handler, items, afterDiscount)
    items = afterDiscount or items
    for k, v in pairs(items) do
        -- doubling the buy price for each item
        v.buy = v.buy * 2
    end
   -- need to return the items table
    return items
end
You would even be able to add new items in the callback function just by inserting them into table
this is a great example of an amazing system at work..... amazing work!
 
Back
Top