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

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
Super, but Guards always they turn down when they are back in position, not in the direction I put them in on the map editor. :(
Where I can change this? Bicouse this is SOUTH guard i need WEST, EAST and NORTH to all gates :p

edit:
I find! Sorry :)

pvpNpcs[npcId].direction = DIRECTION_SOUTH
 
Last edited:
Could someone update the code for RevScripts please?
 
Back
Top