Skip to content

Complete Tutorial ‐ RevScript System for NPCs

Mateuzkl edited this page Apr 28, 2026 · 3 revisions

📋 Comprehensive Guide to NpcsHandler

This tutorial covers everything you need to know about the modern NpcsHandler system, designed for high-performance and declarative NPC scripting.


Crystal Server RevScript Compatibility

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_REV and it will still load.
  • Subfolders are supported automatically.
  • .lua files 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 by data/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.


📑 Index

🏗️ 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

1. Basic Structure

Modern Pattern (Recommended)

-- 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.

Legacy Pattern

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)

2. Creating the NPC Type

NPC Properties

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()

3. Keyword System

Creating Keywords

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 and Farewell

-- 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|!"})

4. Callbacks and Responses

Simple Response

local trade = greet:keyword("trade")
trade:respond("I sell many items!")

Callback Response

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?"
end

Complex Callback

local 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

Special Variables

Variable Description
|PLAYERNAME| Replaced by the player's name
|NPCNAME| Replaced by the NPC's name

5. Requirements

Using Requirements

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!")

Storage Operators

Operator Meaning
"==" Equal
"!=" Not equal
">" Greater than
"<" Less than
">=" Greater or equal
"<=" Less or equal

6. Shop System

Creating a Shop

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 set

7. Travel System

Configuring Destinations

local 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.


8. Storage and Data

-- Set storage on keyword interaction
keyword:setStorageValue(10001, 1)

-- Require storage condition to enter keyword branch
keyword:onStorageValue(10001, 1, "==")

9. Available Functions

Handler

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

Keyword

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

Requirements

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

10. Complete Examples

Example 1 — Simple Greeter NPC

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!")

Example 2 — Promotion NPC

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|.")

Example 3 — Shop NPC

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)

Example 4 — Travel NPC

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)

Notes

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 -1 means the key has never been set.
  • |PLAYERNAME| and |NPCNAME| are automatically replaced in all response strings.
  • Use handler.greetWords to reference the configured greeting keywords inside keyword trees.
  • Sub-keywords only trigger while the player is in conversation with that NPC.

🐛 Troubleshooting

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

Clone this wiki locally