-
Notifications
You must be signed in to change notification settings - Fork 36
Complete Tutorial ‐ RevScript System for NPCs
This tutorial covers everything you need to know about the modern NpcsHandler system, designed for high-performance and declarative NPC scripting.
TFS 1.8-8.60 now loads NPC RevScripts from any direct folder inside data/npc/, except data/npc/lib/, and scans each folder recursively.
Supported layout:
data/npc/
├── lib/
│ ├── npc.lua
│ └── crystalcompat/
├── lua/
├── npc_CRYSTALSERVER_REV/
│ └── subfolder/
│ └── Delowen.lua
└── any_custom_folder/
└── nested/
└── npc.lua
Important details:
- Folder names are not hardcoded. You can rename
npc_CRYSTALSERVER_REVand it will still load. - Subfolders are supported automatically.
-
.luafiles are sorted before loading. - Files with
#in the filename are ignored. -
data/npc/lib/is reserved for libraries and compatibility helpers. - Crystal-style
dofile()calls are resolved dynamically bydata/npc/lib/crystalcompat/.
When an NPC is not found, the engine now prints a detailed English diagnostic explaining that no RevScript was registered in the scanned data/npc/ folders and that the fallback XML file is also missing.
| 🏗️ Foundations | ⚙️ Mechanics | 🎒 Systems | 🆘 Extra |
|---|---|---|---|
| 1. Basic Structure | 3. Keyword System | 6. Shop System | 9. Available Functions |
| 2. NPC Type | 4. Callbacks | 7. Travel System | 10. Complete Examples |
| 5. Requirements | 8. Storage Data | 🐛 Troubleshooting |
-- Create NPC Type
local npcType = Game.createNpcType("NPC Name")
npcType:outfit({lookType = 128, lookHead = 0, lookBody = 0, lookLegs = 0, lookFeet = 0, lookAddons = 0})
npcType:health(100)
npcType:maxHealth(100)
npcType:walkInterval(2000)
npcType:walkSpeed(100)
npcType:spawnRadius(3)
npcType:defaultBehavior()
-- Create Handler
local handler = NpcsHandler("NPC Name")
-- Configure greeting and farewell keywords
handler:setGreetKeywords({"hi", "hello"})
handler:setFarewellKeywords({"bye", "farewell"})
handler:setFarewellResponse({"Farewell, |PLAYERNAME|."})
-- Greeting keyword
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse({"Hello |PLAYERNAME|!"})Important
Why use this pattern?
The NpcsHandler approach is more concise, declarative, and avoids boilerplate event wiring.
It is the recommended approach for all new NPCs in TFS 1.8 downgrade.
local internalNpcName = "NPC Name"
local npcType = Game.createNpcType(internalNpcName)
local npcConfig = {}
npcConfig.name = internalNpcName
npcConfig.description = internalNpcName
npcConfig.health = 100
npcConfig.maxHealth = npcConfig.health
npcConfig.walkInterval = 2000
npcConfig.walkRadius = 2
npcConfig.outfit = {lookType = 128}
npcConfig.flags = {floorchange = false}
local keywordHandler = KeywordHandler:new()
local npcHandler = NpcHandler:new(keywordHandler)
npcType.onAppear = function(npc, creature)
npcHandler:onCreatureAppear(creature)
end
npcType.onDisappear = function(npc, creature)
npcHandler:onCreatureDisappear(creature)
end
npcType.onSay = function(npc, creature, type, message)
npcHandler:onCreatureSay(creature, type, message)
end
npcType.onCloseChannel = function(npc, creature)
npcHandler:onCloseChannel(creature)
end
npcType.onThink = function(npc, interval)
npcHandler:onThink()
end
-- Required callback
local function creatureSayCallback(npc, creature, type, message)
local player = Player(creature)
if not npcHandler:checkInteraction(npc, creature) then
return false
end
return true
end
npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback)
npcHandler:setMessage(MESSAGE_GREET, "Hello |PLAYERNAME|!")
npcHandler:setMessage(MESSAGE_FAREWELL, "Goodbye!")
npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true)
npcType:register(npcConfig)local npcType = Game.createNpcType("NPC Name")
-- Appearance
npcType:outfit({
lookType = 128, -- Outfit type
lookHead = 0, -- Head color (0-132)
lookBody = 0, -- Body color (0-132)
lookLegs = 0, -- Legs color (0-132)
lookFeet = 0, -- Feet color (0-132)
lookAddons = 0 -- Addons (0, 1, 2, 3)
})
-- Attributes
npcType:health(100)
npcType:maxHealth(100)
npcType:walkInterval(2000) -- Movement interval (ms)
npcType:walkSpeed(100) -- Movement speed
npcType:spawnRadius(3) -- Spawn radius
-- Behavior
npcType:defaultBehavior()local handler = NpcsHandler("NPC Name")
-- Simple keyword
local keyword1 = handler:keyword("trade")
-- Multiple keywords
local keyword2 = handler:keyword({"promote", "promotion"})
-- Sub-keyword system
local greet = handler:keyword(handler.greetWords)
local trade = greet:keyword("trade")
local yes = trade:keyword("yes")
local no = trade:keyword("no")-- Greeting words
handler:setGreetKeywords({"hi", "hello", "hey"})
-- Greeting responses
handler:setGreetResponse({
"Hello |PLAYERNAME|!",
"Welcome |PLAYERNAME|!"
})
-- Farewell words
handler:setFarewellKeywords({"bye", "farewell"})
-- Farewell responses
handler:setFarewellResponse({
"Goodbye |PLAYERNAME|!",
"See you later!"
})
-- Greeting keyword
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse({"Hello |PLAYERNAME|!"})local trade = greet:keyword("trade")
trade:respond("I sell many items!")local promote = greet:keyword("promote")
function promote:callback(npc, player, message, handler)
if player:getLevel() < 20 then
return true, "You need level 20!"
end
return true, "Do you want promotion for 20000 gold?"
endlocal yes = promote:keyword("yes")
function yes:callback(npc, player, message, handler)
if player:getLevel() < 20 then
return true, "You need level 20!"
end
local vocation = player:getVocation()
local promotion = vocation:getPromotion()
if not promotion then
return true, "You are already promoted!"
end
if not player:removeMoney(20000) then
return true, "You don't have enough money!"
end
player:setVocation(promotion)
player:setStorageValue(PlayerStorageKeys.promotion, 1)
player:getPosition():sendMagicEffect(CONST_ME_FIREWORK_RED)
return true, "Congratulations! You are now promoted!"
end| Variable | Description |
|---|---|
|PLAYERNAME| |
Replaced by the player's name |
|NPCNAME| |
Replaced by the NPC's name |
local buy = trade:keyword("yes")
buy:respond("Here you go!")
local require = buy:requirements()
-- Minimum level
require:level(20)
-- Premium account
require:premium(true)
-- Remove money
require:removeMoney(10000)
-- Required item
require:item(2148, 100)
-- Remove item
require:removeItem(2148, 100, -1, true)
-- Storage condition
require:storage(10001, 1, "==")
-- Not in PZ lock
require:isPzLocked(false)
-- Not in combat
require:isInfight(false)
-- Custom failure message
buy:failureRespond("You don't meet the requirements!")| Operator | Meaning |
|---|---|
"==" |
Equal |
"!=" |
Not equal |
">" |
Greater than |
"<" |
Less than |
">=" |
Greater or equal |
"<=" |
Less or equal |
local trade = greet:keyword("trade")
trade:respond("Here are my items!")
trade:shop(1)
local shop = NpcShop("NPC Name", 1)
shop:addItem(3031, 100, 50) -- gold coin: buy=100, sell=50
shop:addItem(2152, 10000, 0) -- platinum coin: buy=10000, sell=0
shop:addItem(3003, 0, 50) -- rope: buy=0, sell=50
shop:addDiscount(10001, 10) -- 10% discount if storageKey 10001 is setlocal destinations = {
temple = {
position = Position(1000, 1000, 7),
money = 100,
level = 1,
premium = false
},
darashia = {
position = Position(3276, 3230, 7),
money = 500,
level = 30,
premium = true
}
}
handler:travelTo(destinations)Tip
The keyword "temple", "darashia", etc. will be automatically registered.
Players will be teleported after the money is removed.
-- Set storage on keyword interaction
keyword:setStorageValue(10001, 1)
-- Require storage condition to enter keyword branch
keyword:onStorageValue(10001, 1, "==")| Function | Description |
|---|---|
handler:keyword(words) |
Creates a root-level keyword |
handler:setGreetKeywords(words) |
Sets greeting trigger words |
handler:setFarewellKeywords(words) |
Sets farewell trigger words |
handler:setGreetResponse(texts) |
Sets response on greeting |
handler:setFarewellResponse(texts) |
Sets response on farewell |
handler:travelTo(destinations) |
Registers travel destinations |
| Function | Description |
|---|---|
keyword:respond(text) |
Sets a static response text |
keyword:keyword(words) |
Creates a sub-keyword |
keyword:requirements() |
Returns a requirements object |
keyword:shop(id) |
Opens shop with given ID |
keyword:teleport(position) |
Teleports player to position |
keyword:setStorageValue(key, value) |
Sets storage on interaction |
keyword:onStorageValue(key, value, op) |
Filters by storage condition |
keyword:failureRespond(text) |
Message when requirements fail |
keyword:setGreetResponse(texts) |
Sets greeting response on keyword |
keyword:callback(npc, player, msg, h) |
Custom logic function |
| Function | Description |
|---|---|
require:level(n) |
Minimum player level |
require:premium(bool) |
Premium account required |
require:removeMoney(n) |
Removes gold from player |
require:item(id, count) |
Player must have item |
require:removeItem(id, count, subType, all) |
Removes item from player |
require:storage(key, value, op) |
Storage condition |
require:isPzLocked(bool) |
PZ lock status check |
require:isInfight(bool) |
Combat status check |
local npcType = Game.createNpcType("Town Crier")
npcType:outfit({lookType = 132})
npcType:health(100)
npcType:maxHealth(100)
npcType:walkInterval(3000)
npcType:walkSpeed(80)
npcType:spawnRadius(2)
npcType:defaultBehavior()
local handler = NpcsHandler("Town Crier")
handler:setGreetKeywords({"hi", "hello"})
handler:setFarewellKeywords({"bye"})
handler:setFarewellResponse({"Safe travels, |PLAYERNAME|!"})
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse({"Greetings, |PLAYERNAME|! How can I help you?"})
local news = greet:keyword({"news", "info"})
news:respond("The town is peaceful today. Nothing to report!")local npcType = Game.createNpcType("Guild Promoter")
npcType:outfit({lookType = 140, lookHead = 10, lookBody = 20})
npcType:health(100)
npcType:maxHealth(100)
npcType:walkInterval(2000)
npcType:walkSpeed(100)
npcType:spawnRadius(3)
npcType:defaultBehavior()
local handler = NpcsHandler("Guild Promoter")
handler:setGreetKeywords({"hi", "hello"})
handler:setFarewellKeywords({"bye", "farewell"})
handler:setFarewellResponse({"Farewell, |PLAYERNAME|."})
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse({"Hello |PLAYERNAME|! I can {promote} you."})
local promote = greet:keyword({"promote", "promotion"})
function promote:callback(npc, player, message, handler)
local vocation = player:getVocation()
local promo = vocation:getPromotion()
if not promo then
return true, "You are already promoted, |PLAYERNAME|!"
end
if player:getLevel() < 20 then
return true, "You need to be at least level 20 to be promoted."
end
return true, "I can promote you for 20000 gold. Do you {accept}?"
end
local accept = promote:keyword({"yes", "accept"})
function accept:callback(npc, player, message, handler)
local vocation = player:getVocation()
local promo = vocation:getPromotion()
if not promo then
return true, "You are already promoted!"
end
if player:getLevel() < 20 then
return true, "You still need level 20!"
end
if not player:removeMoney(20000) then
return true, "You don't have enough gold!"
end
player:setVocation(promo)
player:setStorageValue(PlayerStorageKeys.promotion, 1)
player:getPosition():sendMagicEffect(CONST_ME_FIREWORK_RED)
return true, "Congratulations, |PLAYERNAME|! You are now promoted!"
end
local decline = promote:keyword({"no", "cancel"})
decline:respond("Come back when you are ready, |PLAYERNAME|.")local npcType = Game.createNpcType("Equipment Dealer")
npcType:outfit({lookType = 128})
npcType:health(100)
npcType:maxHealth(100)
npcType:walkInterval(2000)
npcType:walkSpeed(100)
npcType:spawnRadius(3)
npcType:defaultBehavior()
local handler = NpcsHandler("Equipment Dealer")
handler:setGreetKeywords({"hi", "hello"})
handler:setFarewellKeywords({"bye"})
handler:setFarewellResponse({"Come back soon, |PLAYERNAME|!"})
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse({"Hello |PLAYERNAME|! I sell fine {equipment}."})
local trade = greet:keyword({"trade", "equipment", "shop", "buy", "sell"})
trade:respond("Browse my wares!")
trade:shop(1)
local shop = NpcShop("Equipment Dealer", 1)
shop:addItem(2400, 100, 50) -- sword
shop:addItem(2412, 200, 100) -- axe
shop:addItem(2470, 50, 20) -- dagger
shop:addItem(2150, 0, 10) -- gold nugget (sell only)local npcType = Game.createNpcType("Ferryman")
npcType:outfit({lookType = 131})
npcType:health(100)
npcType:maxHealth(100)
npcType:walkInterval(0)
npcType:walkSpeed(0)
npcType:spawnRadius(0)
npcType:defaultBehavior()
local handler = NpcsHandler("Ferryman")
handler:setGreetKeywords({"hi", "hello"})
handler:setFarewellKeywords({"bye"})
handler:setFarewellResponse({"Safe journey!"})
local greet = handler:keyword(handler.greetWords)
greet:setGreetResponse({"Hello |PLAYERNAME|! Where would you like to go?"})
local destinations = {
temple = {
position = Position(1000, 1000, 7),
money = 0,
level = 1,
premium = false
},
darashia = {
position = Position(3276, 3230, 7),
money = 500,
level = 30,
premium = false
},
ankrahmun = {
position = Position(3088, 3120, 7),
money = 1000,
level = 40,
premium = true
}
}
handler:travelTo(destinations)Note
- Always call
npcType:defaultBehavior()— without it the NPC will not respond to players. - Callback functions must always
return true, "message". - Requirements are checked before the callback runs.
- A storage value of
-1means the key has never been set. -
|PLAYERNAME|and|NPCNAME|are automatically replaced in all response strings. - Use
handler.greetWordsto reference the configured greeting keywords inside keyword trees. - Sub-keywords only trigger while the player is in conversation with that NPC.
| Problem | Solution |
|---|---|
| NPC not responding at all | Make sure npcType:defaultBehavior() is called |
| Callback not firing | Use function keyword:callback(...) syntax, not a local variable |
| Shop window not opening | Verify the shop ID in trade:shop(id) matches NpcShop("Name", id)
|
| Requirements not blocking | Check operator string: "==", "!=", ">", "<", ">=", "<="
|
| Keyword not matching | Check for typos; keyword matching is case-insensitive but exact |
| Travel not working | Confirm Position(x, y, z) is valid and tile is walkable |
System: RevScript NpcsHandler — Version 1.0 Compatible with: forgottenserver-downgrade-1.8-8.60