Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Revscriptsys] RevNpcSys version 1.0 (lua npcs / NpcType / new npc system) #4671

Merged
merged 52 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5539c3f
lua Npc's
EvilHero90 Apr 30, 2024
1c181d5
tree handling talkstate
EvilHero90 Apr 30, 2024
860580e
Merge branch 'master' into npc-system
EvilHero90 May 1, 2024
7814076
Merge branch 'master' into npc-system
EvilHero90 May 1, 2024
ad194ec
added callback
EvilHero90 May 1, 2024
30ea721
requirements
EvilHero90 May 1, 2024
e2aa611
documentation and splitting in files
EvilHero90 May 2, 2024
cb5d1da
optimizations & better documentation
EvilHero90 May 2, 2024
226419c
requirements, modules & documentation
EvilHero90 May 3, 2024
d004a07
remove unused function & other improvements
EvilHero90 May 3, 2024
d788476
annotations
EvilHero90 May 4, 2024
eac6ea2
Merge branch 'master' into npc-system
EvilHero90 May 5, 2024
683b96b
annotation fixes
EvilHero90 May 5, 2024
81fc9f3
NpcType class
EvilHero90 May 6, 2024
7b15f62
improvements
EvilHero90 May 7, 2024
fe9fb0f
~NpcEventsHandler()
EvilHero90 May 7, 2024
d45354c
clang
EvilHero90 May 7, 2024
29a4605
removing not necessary check
EvilHero90 May 7, 2024
ea11545
improvements on keywords & responses
EvilHero90 May 7, 2024
5acc33f
Merge branch 'master' into npc-system
EvilHero90 May 7, 2024
ffa54e8
added the missing events & callbacks
EvilHero90 May 7, 2024
df23ff9
shop works with item name aswell
EvilHero90 May 8, 2024
4467f40
error handling for shop items
EvilHero90 May 8, 2024
477731a
more functionality and slight fixes
EvilHero90 May 11, 2024
4824668
spelling mistake
EvilHero90 May 11, 2024
3b85b28
changing pointers from shared_ptr to unique_ptr
EvilHero90 May 11, 2024
bf967d4
added NpcVoices & few other things
EvilHero90 May 12, 2024
b580581
sub-keywords
EvilHero90 May 12, 2024
69cd124
re work on requirements
EvilHero90 May 13, 2024
de5c7bc
greeting re work
EvilHero90 May 13, 2024
c74fb12
code cleanup & keyword fix
EvilHero90 May 15, 2024
b629df4
adding onAnswer() & money transfer dupe
EvilHero90 May 15, 2024
2b71c28
check for answer
EvilHero90 May 15, 2024
8dae802
renamed get/add/reset answers into data
EvilHero90 May 16, 2024
eba30a3
code cleanup
EvilHero90 May 16, 2024
17d11e0
clang
EvilHero90 May 16, 2024
37be7c5
Update modules.lua
EvilHero90 May 19, 2024
331e15e
idle rework
EvilHero90 May 19, 2024
42b6302
slight optimizations
EvilHero90 May 19, 2024
f82770e
adding resetData where needed
EvilHero90 May 21, 2024
cbfe62c
reworked NpcType class
EvilHero90 May 23, 2024
75d3c85
improvement on voices
EvilHero90 May 23, 2024
53dbb16
syncing players money on transfer
EvilHero90 May 24, 2024
e6a1c43
renaming folder
EvilHero90 May 26, 2024
4a409f6
Merge branch 'master' into npc-system
EvilHero90 May 27, 2024
ab9c994
merging conflicts
EvilHero90 May 27, 2024
a3a3e66
another merging conflict
EvilHero90 May 27, 2024
21ad5bf
add NpcType to cpplinter.lua
EvilHero90 May 27, 2024
80a793c
correcting parameters in onMove
EvilHero90 May 27, 2024
ff98732
NpcFocus fix
EvilHero90 May 27, 2024
0e4362b
adding vocation requirements
EvilHero90 May 28, 2024
cc3b704
final version 1.0 commit
EvilHero90 May 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions data/lib/compat/compat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,42 @@ do
rawgetmetatable("Action").__newindex = ActionNewIndex
end

do
local function NpcTypeNewIndex(self, key, value)
if key == "onSay" then
self:eventType("say")
self:onSay(value)
return
elseif key == "onDisappear" then
self:eventType("disappear")
self:onDisappear(value)
return
elseif key == "onAppear" then
self:eventType("appear")
self:onAppear(value)
return
elseif key == "onMove" then
self:eventType("move")
self:onMove(value)
return
elseif key == "onPlayerCloseChannel" then
self:eventType("closechannel")
self:onPlayerCloseChannel(value)
return
elseif key == "onPlayerEndTrade" then
self:eventType("endtrade")
self:onPlayerEndTrade(value)
return
elseif key == "onThink" then
self:eventType("think")
self:onThink(value)
return
end
ranisalt marked this conversation as resolved.
Show resolved Hide resolved
rawset(self, key, value)
end
rawgetmetatable("NpcType").__newindex = NpcTypeNewIndex
end

do
local function TalkActionNewIndex(self, key, value)
if key == "onSay" then
Expand Down Expand Up @@ -588,8 +624,6 @@ function isPremium(cid) local p = Player(cid) return p and p:isPremium() or fals

STORAGEVALUE_EMPTY = -1
function Player:getStorageValue(key)
print("[Warning - " .. debug.getinfo(2).source:match("@?(.*)") .. "] Invoking Creature:getStorageValue will return nil to indicate absence in the future. Please update your scripts accordingly.")
ranisalt marked this conversation as resolved.
Show resolved Hide resolved

local v = Creature.getStorageValue(self, key)
return v or STORAGEVALUE_EMPTY
end
Expand Down
100 changes: 100 additions & 0 deletions data/npc/lib/evilnpcsystem/constants.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
--[[
>> Constants <<

Description:
- This file contains all the constants used in the NPC System.
]]

-- MESSAGE_TAGS
---@class MESSAGE_TAGS
---@type table<string, table<string, string|number|function>>
MESSAGE_TAGS = {
playerName = { tag = "|PLAYERNAME|", func = function(params) return params.playerName or "" end },
playerLevel = { tag = "|PLAYERLEVEL|", func = function(params) return params.playerLevel or "" end },
itemCount = { tag = "|ITEMCOUNT|", func = function(params) return params.amount or "" end },
totalCost = { tag = "|TOTALCOST|", func = function(params) return params.total or "" end },
itemName = { tag = "|ITEMNAME|", func = function(params) return params.itemName or "" end },
storageKey = { tag = "|STORAGEKEY|", func = function(params) return params.storage and params.storage.key or "" end },
storageValue = { tag = "|STORAGEVALUE|", func = function(params) return params.storage and params.storage.value or "" end }
}

-- MESSAGE_LIST
---@class MESSAGE_LIST
---@type table<string, string>
MESSAGE_LIST = {
-- shop messages
needMoney = "You need more money",
needSpace = "You do not have enough capacity.",
needMoreSpace = "You do not have enough capacity for all items.",
bought = "You bought |ITEMCOUNT| |ITEMNAME|(s) for |TOTALCOST| gold.",
sold = "You sold |ITEMCOUNT| |ITEMNAME|(s) for |TOTALCOST| gold.",
-- requirement cancel messages
storage = "You do not meet the storage requirement.",
storageToLow = "You do not meet the storage requirement.",
storageToHigh = "You do not meet the storage requirement.",
level = "You need to be exactly level |PLAYERLEVEL|.",
levelToLow = "You need to be atleast level |PLAYERLEVEL|.",
levelToHigh = "You need to be under level |PLAYERLEVEL|.",
premium = "You need to be premium to do this.",
money = "You do not have enough money, it costs |TOTALCOST| gold.",
item = "You do not have atleast |ITEMCOUNT| |ITEMNAME|(s).",
infight = "You need to be in fight.",
notInfight = "You have to be out of fight.",
pzLocked = "You need to be pz locked",
notPzLocked = "You are not allowed to be pz locked"
}

-- KEYWORDS_GREET
---@type string[]
KEYWORDS_GREET = {
"hi",
"hello",
"hey",
"greetings"
}

-- MESSAGES_GREET
---@type string[]
MESSAGES_GREET = {
"Hello |PLAYERNAME| how can I help you?",
"Greetings |PLAYERNAME|, what can I do for you?",
"Hi |PLAYERNAME|, what's your desire today?"
}

-- KEYWORDS_FAREWELL
---@type string[]
KEYWORDS_FAREWELL = {
"bye",
"goodbye",
"farewell",
"cya",
"ciao"
}

-- MESSAGES_FAREWELL
---@type string[]
MESSAGES_FAREWELL = {
"Goodbye |PLAYERNAME|, have a nice day!",
"Farewell |PLAYERNAME|, see you soon!",
"See you later |PLAYERNAME|, take care!"
}

-- FOCUS
---@class FOCUS
---@type table<string, any>
FOCUS = {
-- how long the npc will focus the player in seconds
time = 60,
-- how far the player can step away until the npc loses focus
distance = 5,
-- how near the player has to be to greet the npc
greetDistance = 3
}

-- TALK
---@class TALK
---@type table<string, any>
TALK = {
-- how long in ms the npc will wait before responding
defaultDelay = 1000
}
229 changes: 229 additions & 0 deletions data/npc/lib/evilnpcsystem/events.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
--[[
>> NpcEvents <<

Description:
- This module contains event functions related to NPCs.
- These functions handle the behavior of NPCs when certain events occur, such as when a player appears, disappears, or says something.
- The module provides functions for handling NPC appearances, disappearances, thinking, and speech.
- The functions in this module are called by the NPC system to execute the corresponding behavior.
- This module also includes callbacks that can be used to extend or modify the default behavior of NPCs.
- The module is designed to be used in conjunction with other modules and scripts that define NPC behavior.

Functions:
- NpcEvents.onAppear(npc, creature)
- NpcEvents.onDisappear(npc, creature)
- NpcEvents.onThink(npc)
- NpcEvents.onSay(npc, creature, messageType, message)
- NpcEvents.onAppearCallback(creature)
- NpcEvents.onDisappearCallback(creature)
- NpcEvents.onThinkCallback()
- NpcEvents.onSayCallback(creature, messageType, message)
]]

---@class NpcEvents
---@field onAppear fun(npc: Npc, creature: Creature)
---@field onDisappear fun(npc: Npc, creature: Creature)
---@field onThink fun(npc: Npc): boolean
---@field onSay fun(npc: Npc, creature: Creature, messageType: number, message: string)
---@field onAppearCallback fun(creature: Creature)
---@field onDisappearCallback fun(creature: Creature)
---@field onThinkCallback fun()
---@field onSayCallback fun(creature: Creature, messageType: number, message: string)

-- Make sure we are not overloading on reload
if not NpcEvents then
-- If NpcEvents doesn't exist, it's created as an empty table
NpcEvents = {}
-- onAppear function is called when an NPC appears to a creature (player) or when the creature appears to the NPC.
-- It executes the onAppearCallback function of the NPC if it is defined.
---@param npc Npc The NPC that appeared.
---@param creature Creature The creature (player) that the NPC appeared to.
function NpcEvents.onAppear(npc, creature)
end

-- onDisappear function is called when an NPC disappears from a creature (player) or when the creature disappears from the NPC.
-- It executes the onDisappearCallback function of the NPC if it is defined.
---@param npc Npc The NPC that disappeared.
---@param creature Creature The creature (player) that the NPC disappeared from.
function NpcEvents.onDisappear(npc, creature)
if not creature:isPlayer() then
return
end
local focus = NpcFocus(npc)
if focus:isFocused(creature) then
focus:removeFocus(creature)
local talkQueue = NpcTalkQueue(npc)
talkQueue:clearQueue(creature)
end
end

-- onThink function is called when an NPC thinks.
-- It handles the behavior of the NPC when thinking, such as removing focus from players who are too far away or have been focused for too long.
-- It also processes the talk queue and adjusts the NPC's orientation based on the current focus.
-- It executes the onThinkCallback function of the NPC if it is defined.
---@param npc Npc The NPC that is thinking.
function NpcEvents.onThink(npc)
local handler = NpcsHandler(npc)
local focus = NpcFocus(npc)
for playerid, releaseTime in pairs(focus.focus) do
local player = Player(playerid)
if getDistanceTo(player:getId()) >= FOCUS.distance or releaseTime < os.time() then
focus:removeFocus(player)
closeShopWindow(player)
selfSay(handler.farewellResponses[math.random(1, #handler.farewellResponses)]:replaceTags({playerName = player:getName()}), player)
end
end

local talkQueue = NpcTalkQueue(npc)
talkQueue:processQueue()

if #focus.focus == 0 then
doNpcSetCreatureFocus(nil)
else
local player = focus:getCurrentFocus()
if player then
if player:getPosition().y > npc:getPosition().y and player:getPosition().x == npc:getPosition().x then
selfTurn(DIRECTION_SOUTH)
elseif player:getPosition().y < npc:getPosition().y and player:getPosition().x == npc:getPosition().x then
selfTurn(DIRECTION_NORTH)
elseif player:getPosition().x > npc:getPosition().x and player:getPosition().y == npc:getPosition().y then
selfTurn(DIRECTION_EAST)
elseif player:getPosition().x < npc:getPosition().x and player:getPosition().y == npc:getPosition().y then
selfTurn(DIRECTION_WEST)
end
end
end
end

-- onSay function is called when a creature (player) says something to an NPC.
-- It handles the behavior of the NPC when a creature says something, such as greeting the player, responding to messages, opening shop windows, and executing callbacks.
-- It executes the onSayCallback function of the NPC if it is defined.
-- It checks requirements and modules for the NPC's responses and adjusts the talk state accordingly.
---@param npc Npc The NPC that is being spoken to.
---@param creature Creature The creature (player) that is speaking.
---@param messageType number The type of the message.
---@param message string The message spoken by the creature.
function NpcEvents.onSay(npc, creature, messageType, message)
-- Gracefully return if the talking creature is not a player
if not creature:isPlayer() then
return
end

local message = message:lower()
-- initlialize the handler, focus and talkQueue
local handler = NpcsHandler(npc)
local focus = NpcFocus(npc)
local talkQueue = NpcTalkQueue(npc)

if not focus:isFocused(creature) then
-- If the player is not focused, the NPC will greet the player if the player says a greeting word and is in range
if getDistanceTo(creature:getId()) > FOCUS.greetDistance then
return
end

for _, word in pairs(handler.greetWords) do
if message == word then
focus:addFocus(creature)
doNpcSetCreatureFocus(creature:getId())
local msg = handler.greetResponses[math.random(1, #handler.greetResponses)]:replaceTags({playerName = creature:getName()})
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
handler:setTalkState(handler, creature)
return
end
end
-- didn't greet yet, so we return
return
else
-- If the player is focused, the NPC will say goodbye if the player says a farewell word
for _, word in pairs(handler.farewellWords) do
if message == word then
focus:removeFocus(creature)
closeShopWindow(creature)
local msg = handler.farewellResponses[math.random(1, #handler.farewellResponses)]:replaceTags({playerName = creature:getName()})
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
return
end
end
end

-- Checks if the NPC has a response for the given message
if handler:getTalkState(creature):isKeyword(message) then
-- renewing the focus for the player
focus:addFocus(creature)
-- If the NPC has a response, it sets the talk state to the one associated with the message
handler:setTalkState(handler:getTalkState(creature):isKeyword(message), creature)
-- check if we have a callback for this talk state
if handler:getTalkState(creature).callback then
local ret, failureMessage = handler:getTalkState(creature):callback(npc, creature)
if not ret then
local msg = handler:getTalkState(creature).failureResponse:replaceTags({playerName = creature:getName()})
if failureMessage then
msg = failureMessage:replaceTags({playerName = creature:getName()})
end
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
handler:setTalkState(handler, creature)
return
end
end
-- checking for requirements
local ret, msg = handler:getTalkState(creature):requirements():init(creature)
if not ret then
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
handler:setTalkState(handler, creature)
return
end
-- checking for modules
-- todo implement modules
if handler:getTalkState(creature).teleportPosition then
focus:removeFocus(creature)
closeShopWindow(creature)
local msg = handler:getTalkState(creature):getResponse():replaceTags({playerName = creature:getName()})
selfSay(msg, creature)
creature:teleportTo(handler:getTalkState(creature).teleportPosition)
handler:setTalkState(handler, creature)
return
end
-- If the NPC has a shop for the message, it opens the shop window
if handler:getTalkState(creature):getShop(message) then
handler:setActiveShop(creature, handler:getTalkState(creature):getShop(message))
local shop = NpcShop(npc, handler:getActiveShop(creature))
local items = shop:getItems()
if shop:hasDiscount(creature) then
local afterDiscount = {}
for _, item in pairs(items) do
table.insert(afterDiscount, {
id = item.id, name = item.name,
buy = (item.buy - math.ceil(item.buy / 100 * (shop:hasDiscount(creature) and shop:hasDiscount(creature) or 0))),
sell = item.sell,
subtype = item.subtype == nil and nil or item.subtype
})
end
npc:openShopWindow(creature, afterDiscount, shop.onBuy, shop.onSell)
else
npc:openShopWindow(creature, items, shop.onBuy, shop.onSell)
end
end
-- If the NPC has a response for the current topic, it says the response
if handler:getTalkState(creature):getResponse() then
local msg = handler:getTalkState(creature):getResponse():replaceTags({playerName = creature:getName()})
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
end
-- if the NPC has reached the last keyword, it resets the talk state
if next(handler:getTalkState(creature).keywords) == nil then
handler:setTalkState(handler, creature)
end
-- If the NPC has a resetTalkstate, it resets the talk state
if handler:getTalkState(creature).resetTalkstate then
handler:setTalkState(handler, creature)
end
elseif message == "help" then
-- If the player asks for help, the NPC will respond with the available keywords
local words = {}
for k, v in pairs(handler:getKeywords()) do
table.insert(words, "{".. k .."}")
end
local msg = "I only react to these words: " .. table.concat(words, ", ")
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
end
end
end
Loading
Loading