• 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!

NPC Advance Guard [Attacking Pk's and Monsters with options] [TFS 1.x]

Ok, I find out now,, the problem happen only when there is more than 1 guard on the map
when I added 10 guard by city, it bugs
Yep had the same problem when you're adding multiple guards some guards won't attack.

Also can someone make it attack with physical instead of fire and just follow like a real guard? :p
 
How can i change the speed of the guard? I mean i know how to change it on the XML file. But the guard still only goes 1 step and pauses a bit then another step, very easy to outrun. Not arguing about it, great script to be honest! Just getting my head busted trying to edit it :p
 
Yep had the same problem when you're adding multiple guards some guards won't attack.

Also can someone make it attack with physical instead of fire and just follow like a real guard? :p

It can be found in the code right here @Oualid
Code:
    -- If target is found
    local npcId = npc:getId()
    doTargetCombatHealth(npcId, targetId, COMBAT_FIREDAMAGE, -config.damageValue.min, -config.damageValue.max, CONST_ME_HITBYFIRE)
    npcPosition:sendDistanceEffect(targetPosition, CONST_ANI_FIRE)
    doNpcSetCreatureFocus(targetId)

@Printer this is a really nice script, thanks for sharing man! :)
 
its possible change doteleport to move?
like npc move to attack creature/pk and moving back

i dislike the teleport
 
NPC worked perfectly !! I loved the monsters' throw and it goes back to the initial position, after exceeding the limit of configurable SQM.

I only noticed some details that are:


NPC is attacking even when I go down or up a basement, ladder etc ... If I am still in the range he continues to attack regardless of the Z position.
_____________________________________________________________________________________________________________
After he loses the target, he gets stopped instead of walking again, even after putting in his XML >>>> speed = "500" walkinterval = "2000" <<<<<<


_____________________________________________________________________________________________________________

The same goes for killing a monster, it stops without moving until the next target appears.

_____________________________________________________________________________________________________________
When the player is Red skull or black the npc attacks the player even within the protection zone or house.


_____________________________________________________________________________________________________________

The npc attacks are not respecting the "blockwalls" (serves to block attacks when the target is behind the wall, pillars, closed doors, closed windows etc.)


_____________________________________________________________________________________________________________
Also I would like to know if it is possible to put it to when someone says "hi" it will answer at least what is in its XML.
I currently tested it and it did not work. :(

_____________________________________________________________________________________________________________
And finally leaving the "true" option on will continue attacking the summon.

OBS: I made the summon from the command of the GM / summon name of the monsters.

Thank you again for your attention. : D
 
Hey Printer, first THANKS for an amazing script.
it works great,
although i do have 2 problems, it might be the same tho, first i cant get it to work with spawned npcs from Map. I have to add it with command.
Second, as previously said, it only works with the last added guard.
Adding new NPCs with the same script, gets every other NPC with that script stuck.
coping the script into another NPC and adding only One copy of an NPC per NPC file seems to work. (But i wont be addind dozens of different NPCs.)
if anyone has an idea how to fix this
I've been trying to figure this out, why does the script only work with one NPC at the time. Does it end properly? i mean
if its constantly using the script, then that might cause a problem for other NPCs to use it? idk
something along those lines i think.

help or guidance would be appreciated.
Thanks again,
Danat.
 
erro.png

hey i need help here


function Creature.isAttackable(self)
if not self:isNpc() then
if self:isPlayer() and not self:getGroup():getAccess() then
if config.attackPK.value and isInArray(config.attackPK.skulls, self:getSkull()) then
return true
end
end
 
Here is an improved version of this:
Guard now does melee + shoots bolts and says some shit before charging you:

He walks around his spawn radius like a normal NPC;
And he walks back to the original spawnPosition when the creature is out of range (instead of teleporting).

Bug fixes:
  • Can have multiple PVP npcs
  • The attackSummons flag has been fixed
  • Attacks every 2000ms instead of 1000ms
  • No lag between steps
  • Should function like a normal NPC in all other ways, so can add more keywords, add quests etc and it should work fine
  • Doesn't target creatures that are out of reach (behind walls etc) or in PZ
Known issues:
  • When in battle, NPC does not 'dance' like creatures do
  • Your spawn radius when placing the NPC in your map editor should be equal or greater than config.attackRadius.walkDistance:
    -> attackRadius = {x = 7, y = 5, targetDistance = 3, walkDistance = 7},
    if its not, the NPC will stand still if they walk outside of that spawn radius chasing a monster, but didn't go far enough to trigger a reset:
    -> selfMoveTo(pvpNpcs[npcId].spawnPosition.x, pvpNpcs[npcId].spawnPosition.y, pvpNpcs[npcId].spawnPosition.z)
  • Spawning the NPC via a script or via the /s command only gives it a 3x3 spawn radius so it's not really suitable for that either.
  • /reload npcs sets pvpNpcs[npcId].spawnPosition to their current sqm which can be an issue if they're chasing something.

guard.xml
XML:
<?xml version="1.0" encoding="UTF-8"?>
<npc name="Guard" script="guard.lua" walkinterval="2000" floorchange="0" speed="200">
    <health now="100" max="100" />
    <look type="268" head="76" body="38" legs="76" feet="95" addons="2" />
</npc>


guard.lua
Lua:
local keywordHandler = KeywordHandler:new()
local npcHandler = NpcHandler:new(keywordHandler)
NpcSystem.parseParameters(npcHandler)

local config = {
    attackRadius = {x = 7, y = 5, targetDistance = 3, walkDistance = 7},
    attackPK = {value = true, skulls = {SKULL_WHITE, SKULL_RED}},
    attackMonster = {value = true, ignore = {"Rat", "Cave Rat"}, attackSummons = false},
    meleeDamageValue = {min = 100, max = 250},
    rangedDamageValue = {min = 50, max = 100}
}

-- Cache
local pvpNpcs = {}

-- Can target be attacked
local function isAttackable(self)
    if not self:isNpc() then -- Not NPC
        if self:isPlayer() and not self:getGroup():getAccess() then -- Player
            if config.attackPK.value and isInArray(config.attackPK.skulls, self:getSkull()) then -- Player has skull
                local playerTile = Tile(self:getPosition())
                if playerTile:hasFlag(TILESTATE_PROTECTIONZONE) == false and playerTile:hasFlag(TILESTATE_HOUSE) == false then -- if player not in PZ
                    return true
                end
            end
        end
        if self:isMonster() and config.attackMonster.value then
            local master = self:getMaster()
            if not master then -- Monster
                if not isInArray(config.attackMonster.ignore, self:getName()) then
                    return true
                end
            else -- Summon
                if master:isMonster() then -- always attack monster summons
                    return true
                else
                    if config.attackMonster.attackSummons then -- check for player summons
                        return true
                    end
                end
            end
        end
    end
    return false
end

-- Find target
local function searchTarget(npcId)
    local npc = Npc(npcId)
    if not npc then
        return false
    end
    local attackRadius = config.attackRadius
    for _, spectator in ipairs(Game.getSpectators(npc:getPosition(), false, false, attackRadius.x, attackRadius.x, attackRadius.y, attackRadius.y)) do
        if isAttackable(spectator) then
            if spectator:getPosition():isSightClear(npc:getPosition(), true) and spectator:getPosition():getDistance(pvpNpcs[npcId].spawnPosition) <= attackRadius.walkDistance then
                pvpNpcs[npcId].npcTarget = spectator:getId()
            end
        end
    end
end

-- We're stuck with 1000ms ticks, so make function that we can delay for better visuals
local function rangedAttack(npcId, targetId)
    local npc = Npc(npcId)
    local creature = Creature(targetId)
    if npc and creature then
        doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -config.rangedDamageValue.min, -config.rangedDamageValue.max, CONST_ME_NONE)
        npc:getPosition():sendDistanceEffect(creature:getPosition(), CONST_ANI_BOLT)
    end
end

-- Register PVP-NPC defaults
function onCreatureAppear(self)
    local npc = Npc()
    if npc == self then
        local npc = Npc(self)
        local npcId = npc:getId()
        if not pvpNpcs[npcId] then
            pvpNpcs[npcId] = {}
            pvpNpcs[npcId].spawnPosition = npc:getPosition()
            pvpNpcs[npcId].npcTarget = 0
            pvpNpcs[npcId].melee = true
            pvpNpcs[npcId].ranged = true
            pvpNpcs[npcId].focus = 0
            --pvpNpcs[npcId].timeout = 0
        end
    end
    npcHandler:onCreatureAppear(self)
end
function onCreatureDisappear(cid)        npcHandler:onCreatureDisappear(cid)            end
function onCreatureSay(cid, type, msg)        npcHandler:onCreatureSay(cid, type, msg)        end
function onThink()
    -- Defaults
    local npc = Npc()
    local npcId = npc:getId()
    local npcPosition = npc:getPosition()
    local targetId = 0

    -- Populate npcTarget if it exists
    if pvpNpcs[npcId] then
        targetId = pvpNpcs[npcId].npcTarget
    end

    -- Check target
    local target = Creature(targetId)
    if not target then
        if pvpNpcs[npcId].npcTarget ~= 0 then -- Target just died or logged out
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
        end
        searchTarget(npcId)
        if npcPosition.z ~= pvpNpcs[npcId].spawnPosition.z then -- NPC has accidentally walked up/down stairs
            Creature(npcId):teleportTo(pvpNpcs[npcId].spawnPosition)
            pvpNpcs[npcId].spawnPosition:sendMagicEffect(CONST_ME_TELEPORT)
        end
        --[[
        if npc:getPosition() ~= pvpNpcs[npcId].spawnPosition then
            if pvpNpcs[npcId].timeout < 10 then
                pvpNpcs[npcId].timeout = pvpNpcs[npcId].timeout + 1
            else
                Creature(npcId):teleportTo(pvpNpcs[npcId].spawnPosition)
                pvpNpcs[npcId].spawnPosition:sendMagicEffect(CONST_ME_TELEPORT)
                pvpNpcs[npcId].timeout = 0
            end
        end
        --]]
    else
        -- Reset timeout
        --pvpNpcs[npcId].timeout = 0

        -- Check target is still in range
        local npcPosition = npc:getPosition()
        local targetPosition = target:getPosition()
        local targetTile = Tile(targetPosition)
        if targetTile:hasFlag(TILESTATE_PROTECTIONZONE) or targetTile:hasFlag(TILESTATE_HOUSE) then -- if player in PZ
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
            npcHandler:onThink()
            return
        end
        local offsetX = npcPosition.x - targetPosition.x
        local offsetY = npcPosition.y - targetPosition.y
        local radius = config.attackRadius
        if math.abs(offsetX) <= radius.x and math.abs(offsetY) <= radius.y and npcPosition.z == targetPosition.z then -- If target still onscreen
            if targetPosition:getDistance(pvpNpcs[npcId].spawnPosition) <= radius.walkDistance then -- If NPC walked too far from spawnPosition
                doNpcSetCreatureFocus(targetId)

                -- Targeted voices
                if pvpNpcs[npcId].focus ~= targetId then
                    pvpNpcs[npcId].focus = targetId
                    if target:isPlayer() then
                        local playerVoice = {
                            {"STOP RIGHT THERE! CRIMINAL SCUM", TALKTYPE_YELL},
                            {"Today you shall die... " .. target:getName() .. ".", TALKTYPE_SAY},
                            {"I WILL NOT TOLERATE VIOLENCE HERE!", TALKTYPE_YELL},
                            {"Time to die, fool!", TALKTYPE_SAY},
                            {"It's time to meet my blade - thug!", TALKTYPE_SAY}
                        }
                        local line = math.random(#playerVoice)
                        npc:say(playerVoice[line][1], playerVoice[line][2])
                    elseif target:isMonster() then
                        local monsterVoice = {
                            {"There is a monster inside the town walls!", TALKTYPE_SAY},
                            {"Now how did you get in here?!", TALKTYPE_SAY},
                            {"RUN CITIZENS! A " .. target:getName():upper() .. " HAS BREACHED THE CITY!", TALKTYPE_YELL},
                            {"Quick! slay the " .. target:getName():lower() .. "!", TALKTYPE_SAY},
                            {"I will end you!", TALKTYPE_SAY},
                            {"CHARGE!", TALKTYPE_YELL},
                            {"COME KILL THIS " .. target:getName():upper() .. "!", TALKTYPE_YELL}
                        }
                        local line = math.random(#monsterVoice)
                        npc:say(monsterVoice[line][1], monsterVoice[line][2])
                    end
                end
       
                -- Battle voices
                if pvpNpcs[npcId].melee then -- reuse code that makes 2000ms ticks
                    if math.random(1,10) <= 1 then
                        local battleVoice = {
                            {"I'll make this quick!", TALKTYPE_MONSTER_SAY},
                            {"GUARDS! TO ME!", TALKTYPE_MONSTER_YELL},
                            {"You belong in the ground!", TALKTYPE_MONSTER_SAY},
                            {"Fool!", TALKTYPE_MONSTER_SAY},
                            {"No mercy!", TALKTYPE_MONSTER_SAY},
                            {"You're going to regret that!", TALKTYPE_MONSTER_SAY}
                        }
                        local line = math.random(#battleVoice)
                        npc:say(battleVoice[line][1], battleVoice[line][2])
                    end
                end

                -- Follow Target
                if npcPosition:getDistance(targetPosition) > 1 then
                    selfMoveTo(targetPosition.x, targetPosition.y, targetPosition.z)
                end

                -- Ranged Attack
                if npcPosition:getDistance(targetPosition) <= radius.targetDistance then
                    if pvpNpcs[npcId].ranged == true then
                        if math.random(1,10) > 6 then -- 33% chance to shoot a bolt
                            -- Ranged Hit
                            addEvent(rangedAttack, math.random(1000,1500), npcId, targetId) -- delayed for better visuals
                        end
                        pvpNpcs[npcId].ranged = false -- exhaust for one tick
                    else
                        pvpNpcs[npcId].ranged = true -- toggle exhaust off now
                    end
                end

                -- Melee Attack
                if npcPosition:getDistance(targetPosition) <= 1 then
                    if pvpNpcs[npcId].melee == true then
                        if math.random(1,10) > 2 then -- 80% chance to hit
                            -- Melee Hit
                            doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -config.meleeDamageValue.min, -config.meleeDamageValue.max, CONST_ME_NONE)
                        else -- missed
                            targetPosition:sendMagicEffect(CONST_ME_BLOCKHIT)
                        end
                        pvpNpcs[npcId].melee = false -- exhaust for one tick
                    else
                        pvpNpcs[npcId].melee = true -- toggle exhaust off now
                    end
                end
            else
                -- Reset position walked too far
                selfMoveTo(pvpNpcs[npcId].spawnPosition.x, pvpNpcs[npcId].spawnPosition.y, pvpNpcs[npcId].spawnPosition.z)
                pvpNpcs[npcId].npcTarget = 0
                doNpcSetCreatureFocus(0)
            end
        else
            -- Target not on-screen anymore
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
        end
    end
    npcHandler:onThink()
end

npcHandler:setMessage(MESSAGE_WALKAWAY, 'That was rude.')
npcHandler:setMessage(MESSAGE_FAREWELL, 'Be safe, |PLAYERNAME|.')
npcHandler:setMessage(MESSAGE_GREET, 'Greetings |PLAYERNAME|, I am a keeper of the city.')
npcHandler:addModule(FocusModule:new())
 
Last edited:
how would i make this npc appear if any player killed another player with a difference of lv greater than 50?
 
how would i make this npc appear if any player killed another player with a difference of lv greater than 50?

You'd need to add despawn logic so it doesn't stay there forever.
If you're not up for customizing the code then I'd suggest just summoning a custom monster instead.
 
Here is a slight variant of this
It's cleaner.

The NPC stays in place and doesn't move, like a palace guard.
It returns to its position/direction after its finished attacking.

3JXb43x.gif


Guard South.xml
XML:
<?xml version="1.0" encoding="UTF-8"?>
<npc name="Guard" script="guard_south.lua" walkinterval="0" floorchange="0" speed="200">
    <health now="100" max="100" />
    <look type="268" head="76" body="38" legs="76" feet="95" addons="2" />
</npc>

guard_south.lua
Lua:
local keywordHandler = KeywordHandler:new()
local npcHandler = NpcHandler:new(keywordHandler)
NpcSystem.parseParameters(npcHandler)

local config = {
    attackRadius = {x = 7, y = 5, targetDistance = 3, walkDistance = 7},
    attackPK = {value = true, skulls = {SKULL_WHITE, SKULL_RED}},
    attackMonster = {value = true, ignore = {"Rat", "Cave Rat"}, attackSummons = false},
    meleeDamageValue = {min = 100, max = 250},
    rangedDamageValue = {min = 50, max = 100}
}

-- Cache
local pvpNpcs = {}

-- Can target be attacked
local function isAttackable(self)
    if not self:isNpc() then -- Not NPC
        if self:isPlayer() and not self:getGroup():getAccess() then -- Player
            if config.attackPK.value and isInArray(config.attackPK.skulls, self:getSkull()) then -- Player has skull
                local playerTile = Tile(self:getPosition())
                if playerTile:hasFlag(TILESTATE_PROTECTIONZONE) == false and playerTile:hasFlag(TILESTATE_HOUSE) == false then -- if player not in PZ
                    return true
                end
            end
        end
        if self:isMonster() and config.attackMonster.value then
            local master = self:getMaster()
            if not master then -- Monster
                if not isInArray(config.attackMonster.ignore, self:getName()) then
                    return true
                end
            else -- Summon
                if master:isMonster() then -- always attack monster summons
                    return true
                else
                    if config.attackMonster.attackSummons then -- check for player summons
                        return true
                    end
                end
            end
        end
    end
    return false
end

-- Find target
local function searchTarget(npcId)
    local npc = Npc(npcId)
    if not npc then
        return false
    end
    local attackRadius = config.attackRadius
    for _, spectator in ipairs(Game.getSpectators(npc:getPosition(), false, false, attackRadius.x, attackRadius.x, attackRadius.y, attackRadius.y)) do
        if isAttackable(spectator) then
        -- if spectator:getPosition():isSightClear(npc:getPosition(), true)
            if npc:getPathTo(spectator:getPosition()) and spectator:getPosition():getDistance(pvpNpcs[npcId].spawnPosition) <= attackRadius.walkDistance then
                pvpNpcs[npcId].npcTarget = spectator:getId()   
            end
        end
    end
end

-- We're stuck with 1000ms ticks, so make function that we can delay for better visuals
local function rangedAttack(npcId, targetId)
    local npc = Npc(npcId)
    local creature = Creature(targetId)
    if npc and creature then
        doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -config.rangedDamageValue.min, -config.rangedDamageValue.max, CONST_ME_NONE)
        npc:getPosition():sendDistanceEffect(creature:getPosition(), CONST_ANI_BOLT)
    end
end

-- Register PVP-NPC defaults
function onCreatureAppear(self)
    local npc = Npc()
    if npc == self then
        local npc = Npc(self)
        local npcId = npc:getId()
        if not pvpNpcs[npcId] then
            pvpNpcs[npcId] = {}
            pvpNpcs[npcId].spawnPosition = npc:getPosition()
            pvpNpcs[npcId].npcTarget = 0
            pvpNpcs[npcId].melee = true
            pvpNpcs[npcId].ranged = true
            pvpNpcs[npcId].focus = 0
            pvpNpcs[npcId].direction = DIRECTION_SOUTH
            pvpNpcs[npcId].timeout = 0
            -- Randomize gender and hair colour
            local outfit = Creature(npc:getId()):getOutfit()
            local lookTypes = {
                268,
                269
            }
            local hairTypes = {
                2,
                58,
                12,
                97,
                38,
                37,
                29,
                22,
                95,
                115,
                96,
                39
            }
            outfit.lookType = lookTypes[math.random(1,#lookTypes)]
            outfit.lookHead = hairTypes[math.random(1,#hairTypes)]
            Creature(npc:getId()):setOutfit(outfit)
        end
    end
    npcHandler:onCreatureAppear(self)
end
function onCreatureDisappear(cid)        npcHandler:onCreatureDisappear(cid)            end
function onCreatureSay(cid, type, msg)        npcHandler:onCreatureSay(cid, type, msg)        end
function onThink()
    -- Defaults
    local npc = Npc()
    local npcId = npc:getId()
    local npcPosition = npc:getPosition()
    local targetId = 0

    -- Populate npcTarget if it exists
    if pvpNpcs[npcId] then
        targetId = pvpNpcs[npcId].npcTarget
    end

    -- Check target
    local target = Creature(targetId)
    if not target then -- Not target
        if pvpNpcs[npcId].npcTarget ~= 0 then -- Target just died or logged out
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
        elseif npc:getPosition() == pvpNpcs[npcId].spawnPosition then
            if npc:getDirection() ~= pvpNpcs[npcId].direction then
                npc:setDirection(pvpNpcs[npcId].direction)
            end
            searchTarget(npcId)
        else
            local offsetX = npcPosition.x - pvpNpcs[npcId].spawnPosition.x
            local offsetY = npcPosition.y - pvpNpcs[npcId].spawnPosition.y
            if math.abs(offsetX) <= 1 and math.abs(offsetY) <= 1 then
                Creature(npcId):teleportTo(pvpNpcs[npcId].spawnPosition, true)
            else
                if pvpNpcs[npcId].timeout < 10 then
                    selfMoveTo(pvpNpcs[npcId].spawnPosition.x, pvpNpcs[npcId].spawnPosition.y, pvpNpcs[npcId].spawnPosition.z)
                    pvpNpcs[npcId].timeout = pvpNpcs[npcId].timeout + 1
                else
                    Creature(npcId):teleportTo(pvpNpcs[npcId].spawnPosition)
                    pvpNpcs[npcId].spawnPosition:sendMagicEffect(CONST_ME_TELEPORT)
                    pvpNpcs[npcId].timeout = 0
                end
            end
        end
    else
        -- Reset timeout
        pvpNpcs[npcId].timeout = 0

        -- Check target is still in range
        local targetPosition = target:getPosition()
        local targetTile = Tile(targetPosition)
        if targetTile:hasFlag(TILESTATE_PROTECTIONZONE) or targetTile:hasFlag(TILESTATE_HOUSE) then -- if player in PZ
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
            npcHandler:onThink()
            return
        end
        local offsetX = npcPosition.x - targetPosition.x
        local offsetY = npcPosition.y - targetPosition.y
        local radius = config.attackRadius
        if math.abs(offsetX) <= radius.x and math.abs(offsetY) <= radius.y and npcPosition.z == targetPosition.z then -- If target still onscreen
            if targetPosition:getDistance(pvpNpcs[npcId].spawnPosition) <= radius.walkDistance then -- If NPC walked too far from spawnPosition
                doNpcSetCreatureFocus(targetId)

                -- Targeted voices
                if pvpNpcs[npcId].focus ~= targetId then
                    pvpNpcs[npcId].focus = targetId
                    if target:isPlayer() then
                        local playerVoice = {
                            {"STOP RIGHT THERE! CRIMINAL SCUM", TALKTYPE_YELL},
                            {"Today you shall die... " .. target:getName() .. ".", TALKTYPE_SAY},
                            {"I WILL NOT TOLERATE VIOLENCE HERE!", TALKTYPE_YELL},
                            {"Time to die, fool!", TALKTYPE_SAY},
                            {"It's time to meet my blade - thug!", TALKTYPE_SAY}
                        }
                        local line = math.random(#playerVoice)
                        npc:say(playerVoice[line][1], playerVoice[line][2])
                    elseif target:isMonster() then
                        local monsterVoice = {
                            {"There is a monster inside the town walls!", TALKTYPE_SAY},
                            {"Now how did you get in here?!", TALKTYPE_SAY},
                            {"RUN CITIZENS! A " .. target:getName():upper() .. " HAS BREACHED THE CITY!", TALKTYPE_YELL},
                            {"Quick! slay the " .. target:getName():lower() .. "!", TALKTYPE_SAY},
                            {"I will end you!", TALKTYPE_SAY},
                            {"CHARGE!", TALKTYPE_YELL},
                            {"COME KILL THIS " .. target:getName():upper() .. "!", TALKTYPE_YELL}
                        }
                        local line = math.random(#monsterVoice)
                        npc:say(monsterVoice[line][1], monsterVoice[line][2])
                    end
                end

                -- Battle voices
                if pvpNpcs[npcId].melee then -- reuse code that makes 2000ms ticks
                    if math.random(1,10) <= 1 then
                        local battleVoice = {
                            {"I'll make this quick!", TALKTYPE_MONSTER_SAY},
                            {"GUARDS! TO ME!", TALKTYPE_MONSTER_YELL},
                            {"You belong in the ground!", TALKTYPE_MONSTER_SAY},
                            {"Fool!", TALKTYPE_MONSTER_SAY},
                            {"No mercy!", TALKTYPE_MONSTER_SAY},
                            {"You're going to regret that!", TALKTYPE_MONSTER_SAY}
                        }
                        local line = math.random(#battleVoice)
                        npc:say(battleVoice[line][1], battleVoice[line][2])
                    end
                end

                -- Follow Target
                if npcPosition:getDistance(targetPosition) > 1 then
                    selfMoveTo(targetPosition.x, targetPosition.y, targetPosition.z)
                end

                -- Ranged Attack
                if npcPosition:getDistance(targetPosition) <= radius.targetDistance then
                    if pvpNpcs[npcId].ranged == true then
                        if math.random(1,10) > 6 then -- 33% chance to shoot a bolt
                            -- Ranged Hit
                            addEvent(rangedAttack, math.random(1000,1500), npcId, targetId) -- delayed for better visuals
                        end
                        pvpNpcs[npcId].ranged = false -- exhaust for one tick
                    else
                        pvpNpcs[npcId].ranged = true -- toggle exhaust off now
                    end
                end

                -- Melee Attack
                if npcPosition:getDistance(targetPosition) <= 1 then
                    if pvpNpcs[npcId].melee == true then
                        if math.random(1,10) > 2 then -- 80% chance to hit
                            -- Melee Hit
                            doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -config.meleeDamageValue.min, -config.meleeDamageValue.max, CONST_ME_NONE)
                            -- doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -math.floor(10 / 100 * target:getMaxHealth()), -math.floor(25 / 100 * target:getMaxHealth()), CONST_ME_NONE)
                        else -- missed
                            targetPosition:sendMagicEffect(CONST_ME_BLOCKHIT)
                        end
                        pvpNpcs[npcId].melee = false -- exhaust for one tick
                    else
                        pvpNpcs[npcId].melee = true -- toggle exhaust off now
                    end
                end
            else
                selfMoveTo(pvpNpcs[npcId].spawnPosition.x, pvpNpcs[npcId].spawnPosition.y, pvpNpcs[npcId].spawnPosition.z)
                pvpNpcs[npcId].npcTarget = 0
                doNpcSetCreatureFocus(0)
            end
        else
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
        end
    end
    npcHandler:onThink()
end

npcHandler:setMessage(MESSAGE_WALKAWAY, 'That was rude.')
npcHandler:setMessage(MESSAGE_FAREWELL, 'Be safe, |PLAYERNAME|.')
npcHandler:setMessage(MESSAGE_GREET, 'Greetings |PLAYERNAME|, I am a keeper of the city.')
npcHandler:addModule(FocusModule:new())

How to fix the onLook description:
events\scripts\player.lua
replace:
Lua:
function Player:onLook(thing, position, distance)
    local description = "You see " .. thing:getDescription(distance)
with:
Lua:
function Player:onLook(thing, position, distance)
    local description = "You see " .. thing:getDescription(distance)
    if Npc(thing) then
        if thing:getName() == "Guard" then
            description = "You see a guard."
        end
    end


/reload npcs resets their home position to their current position.
You don't want to do this in production if a guard is off his square chasing something.

Edits:
  • Added voices
  • Added lookType and lookHead randomizer
  • Added a 10 second time-out reset teleport so players can't mess with it.
  • percent damage example: -- doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -math.floor(10 / 100 * target:getMaxHealth()), -math.floor(25 / 100 * target:getMaxHealth()), CONST_ME_NONE)
  • Added onLook changes
 
Last edited:
Here is a slight variant of this
It's cleaner.

The NPC stays in place and doesn't move, like a palace guard.
It returns to its position/direction after its finished attacking.

3JXb43x.gif


Guard South.xml
XML:
<?xml version="1.0" encoding="UTF-8"?>
<npc name="Guard" script="guard_south.lua" walkinterval="0" floorchange="0" speed="200">
    <health now="100" max="100" />
    <look type="268" head="76" body="38" legs="76" feet="95" addons="2" />
</npc>

guard_south.lua
Lua:
local keywordHandler = KeywordHandler:new()
local npcHandler = NpcHandler:new(keywordHandler)
NpcSystem.parseParameters(npcHandler)

local config = {
    attackRadius = {x = 7, y = 5, targetDistance = 3, walkDistance = 7},
    attackPK = {value = true, skulls = {SKULL_WHITE, SKULL_RED}},
    attackMonster = {value = true, ignore = {"Rat", "Cave Rat"}, attackSummons = false},
    meleeDamageValue = {min = 100, max = 250},
    rangedDamageValue = {min = 50, max = 100}
}

-- Cache
local pvpNpcs = {}

-- Can target be attacked
local function isAttackable(self)
    if not self:isNpc() then -- Not NPC
        if self:isPlayer() and not self:getGroup():getAccess() then -- Player
            if config.attackPK.value and isInArray(config.attackPK.skulls, self:getSkull()) then -- Player has skull
                local playerTile = Tile(self:getPosition())
                if playerTile:hasFlag(TILESTATE_PROTECTIONZONE) == false and playerTile:hasFlag(TILESTATE_HOUSE) == false then -- if player not in PZ
                    return true
                end
            end
        end
        if self:isMonster() and config.attackMonster.value then
            local master = self:getMaster()
            if not master then -- Monster
                if not isInArray(config.attackMonster.ignore, self:getName()) then
                    return true
                end
            else -- Summon
                if master:isMonster() then -- always attack monster summons
                    return true
                else
                    if config.attackMonster.attackSummons then -- check for player summons
                        return true
                    end
                end
            end
        end
    end
    return false
end

-- Find target
local function searchTarget(npcId)
    local npc = Npc(npcId)
    if not npc then
        return false
    end
    local attackRadius = config.attackRadius
    for _, spectator in ipairs(Game.getSpectators(npc:getPosition(), false, false, attackRadius.x, attackRadius.x, attackRadius.y, attackRadius.y)) do
        if isAttackable(spectator) then
        -- if spectator:getPosition():isSightClear(npc:getPosition(), true)
            if npc:getPathTo(spectator:getPosition()) and spectator:getPosition():getDistance(pvpNpcs[npcId].spawnPosition) <= attackRadius.walkDistance then
                pvpNpcs[npcId].npcTarget = spectator:getId()  
            end
        end
    end
end

-- We're stuck with 1000ms ticks, so make function that we can delay for better visuals
local function rangedAttack(npcId, targetId)
    local npc = Npc(npcId)
    local creature = Creature(targetId)
    if npc and creature then
        doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -config.rangedDamageValue.min, -config.rangedDamageValue.max, CONST_ME_NONE)
        npc:getPosition():sendDistanceEffect(creature:getPosition(), CONST_ANI_BOLT)
    end
end

-- Register PVP-NPC defaults
function onCreatureAppear(self)
    local npc = Npc()
    if npc == self then
        local npc = Npc(self)
        local npcId = npc:getId()
        if not pvpNpcs[npcId] then
            pvpNpcs[npcId] = {}
            pvpNpcs[npcId].spawnPosition = npc:getPosition()
            pvpNpcs[npcId].npcTarget = 0
            pvpNpcs[npcId].melee = true
            pvpNpcs[npcId].ranged = true
            pvpNpcs[npcId].focus = 0
            pvpNpcs[npcId].direction = DIRECTION_SOUTH
            pvpNpcs[npcId].timeout = 0
            -- Randomize gender and hair colour
            local outfit = Creature(npc:getId()):getOutfit()
            local lookTypes = {
                268,
                269
            }
            local hairTypes = {
                2,
                58,
                12,
                97,
                38,
                37,
                29,
                22,
                95,
                115,
                96,
                39
            }
            outfit.lookType = lookTypes[math.random(1,#lookTypes)]
            outfit.lookHead = hairTypes[math.random(1,#hairTypes)]
            Creature(npc:getId()):setOutfit(outfit)
        end
    end
    npcHandler:onCreatureAppear(self)
end
function onCreatureDisappear(cid)        npcHandler:onCreatureDisappear(cid)            end
function onCreatureSay(cid, type, msg)        npcHandler:onCreatureSay(cid, type, msg)        end
function onThink()
    -- Defaults
    local npc = Npc()
    local npcId = npc:getId()
    local npcPosition = npc:getPosition()
    local targetId = 0

    -- Populate npcTarget if it exists
    if pvpNpcs[npcId] then
        targetId = pvpNpcs[npcId].npcTarget
    end

    -- Check target
    local target = Creature(targetId)
    if not target then -- Not target
        if pvpNpcs[npcId].npcTarget ~= 0 then -- Target just died or logged out
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
        elseif npc:getPosition() == pvpNpcs[npcId].spawnPosition then
            if npc:getDirection() ~= pvpNpcs[npcId].direction then
                npc:setDirection(pvpNpcs[npcId].direction)
            end
            searchTarget(npcId)
        else
            local offsetX = npcPosition.x - pvpNpcs[npcId].spawnPosition.x
            local offsetY = npcPosition.y - pvpNpcs[npcId].spawnPosition.y
            if math.abs(offsetX) <= 1 and math.abs(offsetY) <= 1 then
                Creature(npcId):teleportTo(pvpNpcs[npcId].spawnPosition, true)
            else
                if pvpNpcs[npcId].timeout < 10 then
                    selfMoveTo(pvpNpcs[npcId].spawnPosition.x, pvpNpcs[npcId].spawnPosition.y, pvpNpcs[npcId].spawnPosition.z)
                    pvpNpcs[npcId].timeout = pvpNpcs[npcId].timeout + 1
                else
                    Creature(npcId):teleportTo(pvpNpcs[npcId].spawnPosition)
                    pvpNpcs[npcId].spawnPosition:sendMagicEffect(CONST_ME_TELEPORT)
                    pvpNpcs[npcId].timeout = 0
                end
            end
        end
    else
        -- Reset timeout
        pvpNpcs[npcId].timeout = 0

        -- Check target is still in range
        local targetPosition = target:getPosition()
        local targetTile = Tile(targetPosition)
        if targetTile:hasFlag(TILESTATE_PROTECTIONZONE) or targetTile:hasFlag(TILESTATE_HOUSE) then -- if player in PZ
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
            npcHandler:onThink()
            return
        end
        local offsetX = npcPosition.x - targetPosition.x
        local offsetY = npcPosition.y - targetPosition.y
        local radius = config.attackRadius
        if math.abs(offsetX) <= radius.x and math.abs(offsetY) <= radius.y and npcPosition.z == targetPosition.z then -- If target still onscreen
            if targetPosition:getDistance(pvpNpcs[npcId].spawnPosition) <= radius.walkDistance then -- If NPC walked too far from spawnPosition
                doNpcSetCreatureFocus(targetId)

                -- Targeted voices
                if pvpNpcs[npcId].focus ~= targetId then
                    pvpNpcs[npcId].focus = targetId
                    if target:isPlayer() then
                        local playerVoice = {
                            {"STOP RIGHT THERE! CRIMINAL SCUM", TALKTYPE_YELL},
                            {"Today you shall die... " .. target:getName() .. ".", TALKTYPE_SAY},
                            {"I WILL NOT TOLERATE VIOLENCE HERE!", TALKTYPE_YELL},
                            {"Time to die, fool!", TALKTYPE_SAY},
                            {"It's time to meet my blade - thug!", TALKTYPE_SAY}
                        }
                        local line = math.random(#playerVoice)
                        npc:say(playerVoice[line][1], playerVoice[line][2])
                    elseif target:isMonster() then
                        local monsterVoice = {
                            {"There is a monster inside the town walls!", TALKTYPE_SAY},
                            {"Now how did you get in here?!", TALKTYPE_SAY},
                            {"RUN CITIZENS! A " .. target:getName():upper() .. " HAS BREACHED THE CITY!", TALKTYPE_YELL},
                            {"Quick! slay the " .. target:getName():lower() .. "!", TALKTYPE_SAY},
                            {"I will end you!", TALKTYPE_SAY},
                            {"CHARGE!", TALKTYPE_YELL},
                            {"COME KILL THIS " .. target:getName():upper() .. "!", TALKTYPE_YELL}
                        }
                        local line = math.random(#monsterVoice)
                        npc:say(monsterVoice[line][1], monsterVoice[line][2])
                    end
                end

                -- Battle voices
                if pvpNpcs[npcId].melee then -- reuse code that makes 2000ms ticks
                    if math.random(1,10) <= 1 then
                        local battleVoice = {
                            {"I'll make this quick!", TALKTYPE_MONSTER_SAY},
                            {"GUARDS! TO ME!", TALKTYPE_MONSTER_YELL},
                            {"You belong in the ground!", TALKTYPE_MONSTER_SAY},
                            {"Fool!", TALKTYPE_MONSTER_SAY},
                            {"No mercy!", TALKTYPE_MONSTER_SAY},
                            {"You're going to regret that!", TALKTYPE_MONSTER_SAY}
                        }
                        local line = math.random(#battleVoice)
                        npc:say(battleVoice[line][1], battleVoice[line][2])
                    end
                end

                -- Follow Target
                if npcPosition:getDistance(targetPosition) > 1 then
                    selfMoveTo(targetPosition.x, targetPosition.y, targetPosition.z)
                end

                -- Ranged Attack
                if npcPosition:getDistance(targetPosition) <= radius.targetDistance then
                    if pvpNpcs[npcId].ranged == true then
                        if math.random(1,10) > 6 then -- 33% chance to shoot a bolt
                            -- Ranged Hit
                            addEvent(rangedAttack, math.random(1000,1500), npcId, targetId) -- delayed for better visuals
                        end
                        pvpNpcs[npcId].ranged = false -- exhaust for one tick
                    else
                        pvpNpcs[npcId].ranged = true -- toggle exhaust off now
                    end
                end

                -- Melee Attack
                if npcPosition:getDistance(targetPosition) <= 1 then
                    if pvpNpcs[npcId].melee == true then
                        if math.random(1,10) > 2 then -- 80% chance to hit
                            -- Melee Hit
                            doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -config.meleeDamageValue.min, -config.meleeDamageValue.max, CONST_ME_NONE)
                            -- doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -math.floor(10 / 100 * target:getMaxHealth()), -math.floor(25 / 100 * target:getMaxHealth()), CONST_ME_NONE)
                        else -- missed
                            targetPosition:sendMagicEffect(CONST_ME_BLOCKHIT)
                        end
                        pvpNpcs[npcId].melee = false -- exhaust for one tick
                    else
                        pvpNpcs[npcId].melee = true -- toggle exhaust off now
                    end
                end
            else
                selfMoveTo(pvpNpcs[npcId].spawnPosition.x, pvpNpcs[npcId].spawnPosition.y, pvpNpcs[npcId].spawnPosition.z)
                pvpNpcs[npcId].npcTarget = 0
                doNpcSetCreatureFocus(0)
            end
        else
            pvpNpcs[npcId].npcTarget = 0
            doNpcSetCreatureFocus(0)
            searchTarget(npcId)
        end
    end
    npcHandler:onThink()
end

npcHandler:setMessage(MESSAGE_WALKAWAY, 'That was rude.')
npcHandler:setMessage(MESSAGE_FAREWELL, 'Be safe, |PLAYERNAME|.')
npcHandler:setMessage(MESSAGE_GREET, 'Greetings |PLAYERNAME|, I am a keeper of the city.')
npcHandler:addModule(FocusModule:new())

How to fix the onLook description:
events\scripts\player.lua
replace:
Lua:
function Player:onLook(thing, position, distance)
    local description = "You see " .. thing:getDescription(distance)
with:
Lua:
function Player:onLook(thing, position, distance)
    local description = "You see " .. thing:getDescription(distance)
    if Npc(thing) then
        if thing:getName() == "Guard" then
            description = "You see a guard."
        end
    end


/reload npcs resets their home position to their current position.
You don't want to do this in production if a guard is off his square chasing something.

Edits:
  • Added voices
  • Added lookType and lookHead randomizer
  • Added a 10 second time-out reset teleport so players can't mess with it.
  • percent damage example: -- doTargetCombatHealth(npcId, targetId, COMBAT_PHYSICALDAMAGE, -math.floor(10 / 100 * target:getMaxHealth()), -math.floor(25 / 100 * target:getMaxHealth()), CONST_ME_NONE)
  • Added onLook changes
Really like the alterations! great script!, however for the one that stands still, it seems for me he just defaults to teleporting back, rather then walking.
 
I'm excited to try this ASAP.
Thanks for making this community better.

I've doing some tests. So far, it's been great.
Opens a lot of opportunities.
I only wish NPC's could be attackable or that monsters could fight back.
Anyway, it is great.
 
Last edited:
Using TFS 0.4, probably why i keep getting:
Lua:
[18:52:14.050] [Error - NpcScript Interface]
[18:52:14.050] (Unknown script file)
[18:52:14.051] Description:
[18:52:14.051] attempt to call a nil value
[18:52:14.051] stack traceback:
 
This scripts doesnt work ;/ Npc do nothing when i attack another player next to him ;/
 
Back
Top