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

Tower, waves of monsters.

Xarah

Member
Joined
Apr 18, 2018
Messages
39
Reaction score
7
Hello, I'm trying to create a script for a "Tower." The idea is that once you enter the arena, waves of monsters appear for you to defeat. After defeating three waves, a boss appears. Once you kill the boss, you can move on to the next floor. Right now, the script works in such a way that when you enter the arena, the first wave appears, but after killing that wave, the next one doesn't spawn. I suspect the problem is in the death function, because after killing the first wave, the script behaves as though the monsters are still alive. Please give me some advice on how I can solve this problem.

LUA:
-- Configuration
local config = {
    debug = true, -- Set to true to enable debugging messages

    bossName = "Demon",
    bossSpawnPos = Position(32480, 32189, 7), -- gdzie pojawi się boss
    arenaFromPos = Position(32471, 32184, 7), -- lewy górny róg obszaru areny
    arenaToPos   = Position(32489, 32202, 7), -- prawy dolny róg obszaru areny

    requiredDamagePercent = 5, -- Minimum percentage of damage required
    storages = {
        damageEligibility = 3366 -- Storage for damage eligibility
    },

    -- Definicja fal:
    -- Każda fala ma 'delay' (ile sekund po zabiciu poprzedniej się pojawia)
    -- oraz listę potworów (name i pos)
    waves = {
        [1] = {
            delay = 0,
            monsters = {
                {name = "Rat",    pos = Position(32474, 32193, 7)},
                {name = "Goblin", pos = Position(32486, 32193, 7)}
            }
        },
        [2] = {
            delay = 10,
            monsters = {
                {name = "Minotaur", pos = Position(32474, 32193, 7)},
                {name = "Orc",      pos = Position(32486, 32193, 7)}
            }
        },
        [3] = {
            delay = 10,
            monsters = {
                {name = "Demon Skeleton", pos = Position(32474, 32193, 7)},
                {name = "Ghoul",          pos = Position(32486, 32193, 7)}
            }
        }
    },

    -- Po ilu sekundach od zabicia trzeciej fali pojawia się boss
    bossDelay = 10,

    messages = {
        bossSpawn = "The boss %s has appeared!",
        bossDeath = "Congratulations! You dealt the required damage to pass through the door!",

        needKill = "You need to defeat the boss with sufficient damage to pass!",
        contribute = "You contributed a total of %d damage to defeat the boss!",
        insufficientDamage = "You did not deal enough damage to pass through the door! Required: %d damage.",
        topDamager = "You were the top contributor with %d damage!",

        bossRemoved = "The boss %s was removed due to the absence of players in the arena!",
        bossExists = "The boss is already in the arena! You need to defeat it before summoning another one!",

        waveSpawn  = "Wave %d has appeared!"
    }
}

--------------------------------------------------------------------------------
-- DEBUG FUNCTION
--------------------------------------------------------------------------------
local function debugLog(message, ...)
    if config.debug then
        print("[DEBUG]", string.format(message, ...))
    end
end

--------------------------------------------------------------------------------
-- STAN ARENY
--------------------------------------------------------------------------------
local playersInArena = {}    -- [playerId] = true
local damageTracker = {}     -- mapowanie obrażeń, ale TYLKO na bossa
local bossInArena = nil      -- creatureId bossa (nil = brak)
local waveNumber = 0         -- 0 = fale nie rozpoczęte
local currentWaveMonsters = {}  -- potwory bieżącej fali

--------------------------------------------------------------------------------
-- FUNKCJE POMOCNICZE: GRACZE / ARENA
--------------------------------------------------------------------------------
local function addPlayerToArena(player)
    local pid = player:getId()
    if not playersInArena[pid] then
        playersInArena[pid] = true
        debugLog("Player added to arena: %s", player:getName())
    end
end

local function removePlayerFromArena(player)
    local pid = player:getId()
    if playersInArena[pid] then
        -- sprawdzamy, czy faktycznie wyszedł z obszaru
        if not player:getPosition():isInRange(config.arenaFromPos, config.arenaToPos) then
            playersInArena[pid] = nil
            debugLog("Player removed from arena: %s", player:getName())
        end
    end
end

local function isAnyoneInArena()
    for pid,_ in pairs(playersInArena) do
        local p = Player(pid)
        if p and p:getPosition():isInRange(config.arenaFromPos, config.arenaToPos) then
            return true
        end
    end
    return false
end

-- Usuwa potwory z bieżącej fali
local function removeAllWaveMonsters()
    for _, mob in pairs(currentWaveMonsters) do
        if mob and mob:isMonster() then
            mob:remove()
        end
    end
    currentWaveMonsters = {}
end

--------------------------------------------------------------------------------
-- RESET ARENY (fale, boss, gracze)
--------------------------------------------------------------------------------
local function resetArena()
    waveNumber = 0
    removeAllWaveMonsters()
    currentWaveMonsters = {}
    damageTracker = {}
    if bossInArena then
        local boss = Creature(bossInArena)
        if boss then
            boss:remove()
        end
        bossInArena = nil
    end
    playersInArena = {}
end

--------------------------------------------------------------------------------
-- SPAWNOWANIE FALI
--------------------------------------------------------------------------------
local function spawnWave(waveIndex)
    local waveInfo = config.waves[waveIndex]
    if not waveInfo then
        return
    end

    debugLog("Spawning wave %d ...", waveIndex)
    currentWaveMonsters = {}
    for _, mobInfo in ipairs(waveInfo.monsters) do
        local monster = Game.createMonster(mobInfo.name, mobInfo.pos)
        if monster then
            table.insert(currentWaveMonsters, monster)
        end
    end

    -- Komunikat do graczy w arenie
    for pid,_ in pairs(playersInArena) do
        local p = Player(pid)
        if p then
            p:sendTextMessage(MESSAGE_INFO_DESCR,
                string.format(config.messages.waveSpawn, waveIndex))
        end
    end
end

--------------------------------------------------------------------------------
-- SPRAWDZANIE, CZY WSZYSTKIE POTWORY FALI ZGINĘŁY
--------------------------------------------------------------------------------
local function areAllWaveMonstersDead()
    for _, mob in pairs(currentWaveMonsters) do
        if mob and mob:isMonster() and mob:getHealth() > 0 then
            return false
        end
    end
    return true
end

--------------------------------------------------------------------------------
-- FUNKCJA WYWOŁYWANA PO ŚMIERCI POTWORÓW FALI
--------------------------------------------------------------------------------
local function checkWaveClear()
    if not areAllWaveMonstersDead() then
        return
    end

    debugLog("Wave %d is clear!", waveNumber)
    -- Jeśli to nie ostatnia fala, jedziemy z kolejną
    if waveNumber < 3 then
        waveNumber = waveNumber + 1
        local waveInfo = config.waves[waveNumber]
        if waveInfo then
            addEvent(function()
                if isAnyoneInArena() then
                    spawnWave(waveNumber)
                else
                    resetArena()
                end
            end, waveInfo.delay * 1000)
        end
    else
        -- Była 3 fala, pora na bossa
        addEvent(function()
            if isAnyoneInArena() then
                spawnBoss()
            else
                resetArena()
            end
        end, config.bossDelay * 1000)
    end
end

--------------------------------------------------------------------------------
-- FUNKCJA SPAWN BOSSA
--------------------------------------------------------------------------------
local function spawnBoss(playerWhoRequested)
    -- jeżeli jest boss, nie stwarzamy nowego
    if bossInArena then
        if playerWhoRequested then
            playerWhoRequested:sendTextMessage(MESSAGE_INFO_DESCR, config.messages.bossExists)
        end
        return
    end

    local boss = Game.createMonster(config.bossName, config.bossSpawnPos)
    if boss then
        bossInArena = boss:getId()
        boss:registerEvent("Boss_Waves")
        debugLog("Boss spawned: %s", config.bossName)

        for pid, _ in pairs(playersInArena) do
            local p = Player(pid)
            if p then
                p:sendTextMessage(MESSAGE_INFO_DESCR,
                    string.format(config.messages.bossSpawn, config.bossName))
            end
        end
    end
end

--------------------------------------------------------------------------------
-- ROZPOCZĘCIE FAL
--------------------------------------------------------------------------------
local function startWaves()
    if waveNumber ~= 0 or bossInArena then
        -- Już trwa
        return
    end
    waveNumber = 1
    spawnWave(waveNumber)
end

--------------------------------------------------------------------------------
-- CREATUREEVENT: ŚMIERĆ BOSSA (LICZENIE DMG)
--------------------------------------------------------------------------------
local bossArenaEvent = CreatureEvent("Boss_Waves")
function bossArenaEvent.onDeath(creature, corpse, lastHitKiller, mostDamageKiller)
    -- sprawdzamy, czy ginie nasz boss
    if creature:getName():lower() == config.bossName:lower() then
        local topDamage = 0
        local topDamagerId = nil
        local totalDamage = 0

        for attackerId, damageData in pairs(creature:getDamageMap()) do
            local totalPlayerDamage = damageData.total or 0
            damageTracker[attackerId] = (damageTracker[attackerId] or 0) + totalPlayerDamage
            totalDamage = totalDamage + totalPlayerDamage

            if damageTracker[attackerId] > topDamage then
                topDamage = damageTracker[attackerId]
                topDamagerId = attackerId
            end
        end

        debugLog("Total Damage: %d", totalDamage)
        local requiredDamage = math.ceil(totalDamage * (config.requiredDamagePercent / 100))
        debugLog("Required Damage: %d", requiredDamage)

        for attackerId, totalPlayerDamage in pairs(damageTracker) do
            local player = Player(attackerId)
            if player then
                debugLog("Player: %s, Damage: %d", player:getName(), totalPlayerDamage)
                player:sendTextMessage(MESSAGE_INFO_DESCR,
                    string.format(config.messages.contribute, totalPlayerDamage))

                if totalPlayerDamage >= requiredDamage then
                    player:setStorageValue(config.storages.damageEligibility, 1)
                    player:sendTextMessage(MESSAGE_INFO_DESCR, config.messages.bossDeath)
                else
                    player:sendTextMessage(MESSAGE_INFO_DESCR,
                        string.format(config.messages.insufficientDamage, requiredDamage))
                end
            end
        end

        if topDamagerId then
            local topP = Player(topDamagerId)
            if topP then
                topP:sendTextMessage(MESSAGE_INFO_DESCR,
                    string.format(config.messages.topDamager, topDamage))
            end
        end

        bossInArena = nil
        damageTracker = {}
        resetArena()
    else
        -- to nie boss, może to potwór z fali
        addEvent(checkWaveClear, 200)
    end
    return true
end
bossArenaEvent:register()

--------------------------------------------------------------------------------
-- GLOBAL EVENT: SPRAWDZA, CZY GRACZE SĄ NA ARENIE
--------------------------------------------------------------------------------
local checkArena = GlobalEvent("CheckArenaWithWaves")
function checkArena.onThink(interval)
    -- jeśli fale trwają albo jest boss, a gracze wyszli -> reset
    if waveNumber > 0 or bossInArena then
        if not isAnyoneInArena() then
            debugLog("Arena is empty, removing monsters/boss.")
            resetArena()
        end
    end
    return true
end
checkArena:interval(10000)
checkArena:register()

--------------------------------------------------------------------------------
-- MOVEEVENT: WEJŚCIE NA ARENĘ (AID=4110)
--------------------------------------------------------------------------------
local entrance = MoveEvent()
function entrance.onStepIn(creature, item, position, fromPosition)
    local player = creature:getPlayer()
    if not player then
        return true
    end

    addPlayerToArena(player)
    -- zamiast spawnBoss, startujemy fale, jeśli waveNumber=0 i bossInArena=nil
    if waveNumber == 0 and not bossInArena then
        startWaves()
    end

    return true
end
entrance:type("stepin")
entrance:aid(4110)
entrance:register()

--------------------------------------------------------------------------------
-- MOVEEVENT: WYJŚCIE Z ARENY (AID=4110)
--------------------------------------------------------------------------------
local exit = MoveEvent()
function exit.onStepOut(creature, item, position, fromPosition)
    local player = creature:getPlayer()
    if not player then
        return true
    end

    if not position:isInRange(config.arenaFromPos, config.arenaToPos) then
        removePlayerFromArena(player)
    end
    return true
end
exit:type("stepout")
exit:aid(4110)
exit:register()

--------------------------------------------------------------------------------
-- BLOKADA DRZWI (AID=4111)
--------------------------------------------------------------------------------
local doorCheck = MoveEvent()
function doorCheck.onStepIn(creature, item, position, fromPosition)
    local player = creature:getPlayer()
    if not player then
        return true
    end

    -- Sprawdzamy, czy ma storage = 1
    if player:getStorageValue(config.storages.damageEligibility) ~= 1 then
        player:sendTextMessage(MESSAGE_INFO_DESCR, config.messages.needKill)
        player:teleportTo(fromPosition)
        player:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
        return false
    end

    return true
end
doorCheck:type("stepin")
doorCheck:aid(4111)
doorCheck:register()
 
Solution
You register Boss_Waves event only for boss and it's only function that calls checkWaveClear, so checkWaveClear probably does not execute at all, when you kill normal monsters. Add some debug print at start of checkWaveClear and check, if it executes at all.

EDIT:
You can fix it for test by adding globalevent onThink that will execute 1 time per second and call checkWaveClear.
Real fix would be with onDeath event added to each spawned monster and calling checkWaveClear in it.

It also looks like your code will crash OTS, because of bug described here (you cannot store in Lua objects from C++ that may be removed, as checking "if object is removed" in C++ is impossible and...
a idea im having is not even using a onDeath function, i have a similar script that counts the amounts of monsters with GetSpectators, when that number reaches 0 thats when second wave should spawn.

Arguably onDeath function is by far alot better but the getSpectators works perfectly fine for me so i have no reason to change it (YET).
 
Last edited:
You register Boss_Waves event only for boss and it's only function that calls checkWaveClear, so checkWaveClear probably does not execute at all, when you kill normal monsters. Add some debug print at start of checkWaveClear and check, if it executes at all.

EDIT:
You can fix it for test by adding globalevent onThink that will execute 1 time per second and call checkWaveClear.
Real fix would be with onDeath event added to each spawned monster and calling checkWaveClear in it.

It also looks like your code will crash OTS, because of bug described here (you cannot store in Lua objects from C++ that may be removed, as checking "if object is removed" in C++ is impossible and itself crashes server): [TFS 1.x+] How to NOT write LUA scripts or how to crash server by LUA script (https://otland.net/threads/tfs-1-x-how-to-not-write-lua-scripts-or-how-to-crash-server-by-lua-script.271018/)
Current wave of monsters is stored in currentWaveMonsters as Monster objects, but it should be stored as their IDs, so:
LUA:
table.insert(currentWaveMonsters, monster)
should be:
LUA:
table.insert(currentWaveMonsters, monster:getId())
then areAllWaveMonstersDead should be:
LUA:
local function areAllWaveMonstersDead()
    for _, mobId in pairs(currentWaveMonsters) do
        if Monster(mobId) then
            return false
        end
    end
    return true
end
Monster(id) will try to load monster by ID in C++ and it will work only, if monster is still in RAM, so it will work only for monsters that are alive.

Updated removeAllWaveMonsters:
LUA:
local function removeAllWaveMonsters()
    for _, mobId in pairs(currentWaveMonsters) do
        local mob = Monster(mobId)
        if mob then
            mob:remove()
        end
    end
    currentWaveMonsters = {}
end

Arguably onDeath function is by far alot better but the getSpectators works perfectly fine for me so i have no reason to change it
It depends on boss arena size. For small boss rooms like 20x20 SQM it is ok, but for big arenas like 100x100 SQM or with multiple floors, it will use a lot of CPU to list all monsters by getSpectators.
There are 2 efficient ways:
1. for events (boss arena etc.) with up to ~300 monsters, check every second, if any monster from spawnedMonstersList is still alive with Monster(id) - wastes a bit of CPU every second, but it's very simple code; that's code I've posted above
2. for events with more monsters, add onDeath event to all spawned monsters and remove monster ID from table spawnedMonstersList, when it dies, in onDeath check if table is empty and then execute code for next wave - this code does not waste any CPU, when arena is not active or no one is killing arena monsters
 
Last edited:
Solution
Back
Top