--------------------------------------------------------------------------------
---------------------------------- # CONFIG # ----------------------------------
--------------------------------------------------------------------------------
local WaveEvent = {
MAX_WAVES = 3,
----------------------------
STATE_CLOSED = -1,
STATE_STARTED = 1,
STATE_WAITING = 2,
----------------------------
STATE_STORAGE = 101192,
WAVE_ROUND_STORAGE = 101193,
PLAYERS_JOINED = 101194,
DMG_OVERFLOW = 101195,
REWARD_CHEST_USED = 101196,
REWARD_DIFFICULTY = 101197,
-----------------------------
WAITING_TIME = 1 * 10 * 1000,
MIN_REQ_PLAYERS = 1,
----------------------------
NEXT_WAVE_DELAY = 10 * 1000,
BOSS_SPAWN_DELAY = 5 * 1000,
BOSS_SPAWN_TRIES = 5,
----------------------------
FINAL_BOSS = 'Zugurosh',
mobsPerPlayer = 2,
----------------------------
EXP_REWARD_FACTOR = 1.05, -- 5%
REWARD_CHEST_AID = 10129,
REWARDS = {
[1] = { -- easy
-- {itemid, count, chance}
{2160, 10, 100},
{2159, 5, 5}
},
[2] = { -- medium
{2392, 1, 25},
{2645, 1, 32}
},
[3] = { -- hard
{2520, 1, 100},
{2470, 1, 100}
}
},
MONSTERS = {
HP_INCREMENT = 1.05, -- +5%
DMG_INCREMENT = 1.05, -- +5%
INITIAL_COUNT = 3,
monsterList = {
'armenius','barbaria','thalas'
--'orc warrior'
-- 'rat','snake','orc','rotworm','troll','slime','skeleton','orc warrior','wyvern',
-- 'carrion worm','cyclops','cyclops drone','orc shaman','orc spearman','vampire',
-- 'orc warlord','orc berserker','dragon','fire elemental','energy elemental','wyrm',
-- 'cyclops smith','plaguesmith'
}
},
Arena = {
TOP_LEFT = Position(47, 399, 7),
BOTTOM_RIGHT = Position(61, 409, 7),
monsters = {} -- leave this empty
},
WaitingRoom = {
TOP_LEFT = Position(52, 395, 7),
BOTTOM_RIGHT = Position(55, 397, 7)
},
RewardRoom = Position(58, 397, 7)
}
local function countMonstersInEvent()
local counter = 0
for cid, monster in pairs(WaveEvent.Arena.monsters) do
if monster then
counter = counter + 1
end
end
return counter
end
local function getRandomPosition(conf)
return Position(
math.random(conf.TOP_LEFT.x, conf.BOTTOM_RIGHT.x),
math.random(conf.TOP_LEFT.y, conf.BOTTOM_RIGHT.y),
conf.TOP_LEFT.z
)
end
local function secondsToReadable(s)
local minutes = math.floor(math.mod(s, 3600)/60)
local seconds = math.floor(math.mod(s, 60))
return (minutes > 0 and (minutes .. ' minutes ') or '') ..
(seconds > 0 and (seconds .. ' seconds ') or '')
end
local function ordinal_number(n)
-- https://stackoverflow.com/a/20726654
last_digit = n % 10
if last_digit == 1 and n ~= 11
then return 'st'
elseif last_digit == 2 and n ~= 12
then return 'nd'
elseif last_digit == 3 and n ~= 13
then return 'rd'
else
return 'th'
end
end
--------------------------------------------------------------------------------
-------------------------------- # TALKACTION # --------------------------------
--------------------------------------------------------------------------------
local ta = TalkAction('!joinwave')
function ta.onSay(player, words, param)
local waveEventState = Game.getStorageValue(WaveEvent.STATE_STORAGE)
if not waveEventState or waveEventState == WaveEvent.STATE_CLOSED then
player:sendTextMessage(MESSAGE_STATUS_SMALL, 'The event hasn\'t started.')
return false
end
if waveEventState == WaveEvent.STATE_STARTED then
player:sendTextMessage(MESSAGE_STATUS_SMALL, 'You are late. Catch the train next time.')
return false
end
if player:getZone() ~= ZONE_PROTECTION then
player:sendTextMessage(MESSAGE_STATUS_SMALL, 'Go to a safe zone first.')
return false
end
player:setStorageValue(WaveEvent.REWARD_CHEST_USED, 0)
player:teleportTo(getRandomPosition(WaveEvent.WaitingRoom))
local playersJoined = (Game.getStorageValue(WaveEvent.PLAYERS_JOINED) or 0) + 1
Game.broadcastMessage(
player:getName() .. ' has joined the wave event. ('.. playersJoined ..'/'.. WaveEvent.MIN_REQ_PLAYERS ..')',
MESSAGE_STATUS_CONSOLE_BLUE
)
Game.setStorageValue(WaveEvent.PLAYERS_JOINED, playersJoined)
return false
end
ta:separator(' ')
ta:register()
--------------------------------------------------------------------------------
-------------------------------- # GLOBALEVENT # -------------------------------
--------------------------------------------------------------------------------
local function closeEvent()
Game.setStorageValue(WaveEvent.STATE_STORAGE, WaveEvent.STATE_CLOSED)
Game.setStorageValue(WaveEvent.WAVE_ROUND_STORAGE, 0)
Game.setStorageValue(WaveEvent.PLAYERS_JOINED, 0)
for cid, creature in pairs(WaveEvent.Arena.monsters) do
creature:remove()
end
WaveEvent.Arena.monsters = {}
end
local function isInEvent(player)
local playerPos = player:getPosition()
if (playerPos.x >= WaveEvent.Arena.TOP_LEFT.x and playerPos.x <= WaveEvent.Arena.BOTTOM_RIGHT.x)
and (playerPos.y >= WaveEvent.Arena.TOP_LEFT.y and playerPos.y <= WaveEvent.Arena.BOTTOM_RIGHT.y) then
return true
end
return false
end
local function getPlayersInEvent()
local onlinePlayers = Game.getPlayers()
local playersInEvent = {}
for i, player in pairs(onlinePlayers) do
if isInEvent(player) then
table.insert(playersInEvent, i, player)
end
end
return playersInEvent
end
local function spawnBoss(effect_rounds)
if not effect_rounds then
addEvent(spawnBoss, 1000, WaveEvent.BOSS_SPAWN_TRIES)
return
end
local pos = getRandomPosition(WaveEvent.Arena)
if effect_rounds == 0 then
local m = Game.createMonster(WaveEvent.FINAL_BOSS, getRandomPosition(WaveEvent.Arena), false, true)
m:say('GROOAAAAAR! <dramatic msg here>', TALKTYPE_MONSTER_SAY)
pos:sendMagicEffect(CONST_ME_TELEPORT)
return
end
pos:sendMagicEffect(CONST_ME_TELEPORT)
pos:sendMagicEffect(CONST_ME_POFF)
addEvent(spawnBoss, 1000, effect_rounds - 1)
end
local function handleMonsterCreation(m, wave_n, playerAmount)
local mType = MonsterType(m)
if not mType then
return print('[Warning - WaveEvent::spawnMonsters] Unknown monster type ' .. m)
end
local mPos = getRandomPosition(WaveEvent.Arena)
local monster = Game.createMonster(m, mPos, false, true)
if not monster then
return print('[Warning - WaveEvent::spawnMonsters] Could not create monster ' .. m)
end
monster:setMaxHealth(math.floor((monster:getMaxHealth() * (playerAmount + 1) * WaveEvent.MONSTERS.HP_INCREMENT) / 2))
monster:setHealth(monster:getMaxHealth())
mPos:sendMagicEffect(CONST_ME_TELEPORT)
WaveEvent.Arena.monsters[monster:getId()] = monster
end
local function initWave(wave_n, playerAmount)
if wave_n > WaveEvent.MAX_WAVES then
local mType = MonsterType(WaveEvent.FINAL_BOSS)
if not mType then
print('[Error - WaveEvent::initWave] Could not create final boss. Unknown monster type ('.. WaveEvent.FINAL_BOSS ..')')
-- kick players
return
end
spawnBoss()
return
end
local monsterCount = (wave_n * 2) + WaveEvent.MONSTERS.INITIAL_COUNT
for i = 1, monsterCount do
local m = WaveEvent.MONSTERS.monsterList[math.random(1, #WaveEvent.MONSTERS.monsterList)]
handleMonsterCreation(m, wave_n, playerAmount)
end
end
local function teleportPlayersToArena()
local playersFound = 0
local z = WaveEvent.WaitingRoom.TOP_LEFT.z
for x = WaveEvent.WaitingRoom.TOP_LEFT.x, WaveEvent.WaitingRoom.BOTTOM_RIGHT.x do
for y = WaveEvent.WaitingRoom.TOP_LEFT.y, WaveEvent.WaitingRoom.BOTTOM_RIGHT.y do
local tile = Tile(x, y, z)
local tileCreatures = tile:getCreatures()
if tileCreatures then
for _, creature in pairs(tileCreatures) do
creature:teleportTo(getRandomPosition(WaveEvent.Arena))
playersFound = playersFound + 1
end
end
end
end
if playersFound == 0 then
Game.broadcastMessage('[WaveEvent] Nobody joined the wave event. Closing.', MESSAGE_STATUS_CONSOLE_BLUE)
Game.setStorageValue(WaveEvent.STATE_STORAGE, WaveEvent.STATE_CLOSED)
return
end
if playersFound < WaveEvent.MIN_REQ_PLAYERS then
Game.broadcastMessage('[WaveEvent] Not enough players. Closing.', MESSAGE_STATUS_CONSOLE_BLUE)
Game.setStorageValue(WaveEvent.STATE_STORAGE, WaveEvent.STATE_CLOSED)
return
end
Game.setStorageValue(WaveEvent.STATE_STORAGE, WaveEvent.STATE_STARTED)
Game.broadcastMessage(
'[WaveEvent] Starting with ' .. playersFound .. ' player'
.. (playersFound > 1 and 's' or '')
.. '. Good luck!',
MESSAGE_STATUS_CONSOLE_BLUE
)
Game.setStorageValue(WaveEvent.WAVE_ROUND_STORAGE, 1)
initWave(1, playersFound)
end
local ge = GlobalEvent('WaveEvent')
function ge.onTime(interval)
local eventState = Game.getStorageValue(WaveEvent.STATE_STORAGE)
if eventState == WaveEvent.STATE_STARTED
or eventState == WaveEvent.STATE_WAITING then
print('[Error - WaveEvent::onTime] Event state not closed. StorageKey -> '.. WaveEvent.STATE_STORAGE)
return true
end
Game.broadcastMessage(
'WaveEvent has started. You have '
.. secondsToReadable(WaveEvent.WAITING_TIME / 1000)
.. ' to join. !joinwave',
MESSAGE_STATUS_CONSOLE_BLUE
)
Game.setStorageValue(WaveEvent.STATE_STORAGE, WaveEvent.STATE_WAITING)
addEvent(teleportPlayersToArena, WaveEvent.WAITING_TIME)
end
ge:time('03:49:30')
ge:register()
--------------------------------------------------------------------------------
-------------------------------- # CREATURESCRIPT # ----------------------------
--------------------------------------------------------------------------------
local cs = CreatureEvent('WaveEventPrepareDeath')
function cs.onPrepareDeath(player, killer)
if Game.getStorageValue(WaveEvent.STATE_STORAGE) == WaveEvent.STATE_CLOSED then
return true
end
if isInEvent(player) then
player:teleportTo(player:getTown():getTemplePosition())
player:setHealth(player:getMaxHealth())
player:setMana(player:getMaxMana())
Game.broadcastMessage('[WaveEvent] ' .. player:getName() .. ' was killed by a monster.')
end
local isEventClosed = Game.getStorageValue(WaveEvent.STATE_STORAGE) ~= WaveEvent.STATE_CLOSED
if #getPlayersInEvent() == 0 and not isEventClosed then
Game.broadcastMessage('[WaveEvent] Nobody has completed the wave event. Closing ...')
closeEvent()
end
return true
end
cs:register()
local csx = CreatureEvent('WaveEventMonsterDeath')
function csx.onKill(player, target)
if Game.getStorageValue(WaveEvent.STATE_STORAGE) == WaveEvent.STATE_CLOSED
or not isInEvent(player)
or target:getMaster() then
return true
end
if target:getName():lower() == WaveEvent.FINAL_BOSS:lower() then
Game.broadcastMessage('[WaveEvent] The final boss has been killed. Congratulations!')
Game.setStorageValue(WaveEvent.STATE_STORAGE, WaveEvent.STATE_CLOSED)
local pie = getPlayersInEvent()
-- What was the difficulty ?
local joined = Game.getStorageValue(WaveEvent.PLAYERS_JOINED)
local diff = math.abs(#pie - joined)
if diff <= math.floor(joined * 0.33) then diff = 1
elseif diff <= math.floor(joined * 0.66) then diff = 2
else diff = 3 end
Game.setStorageValue(WaveEvent.REWARD_DIFFICULTY, diff)
for _, p in pairs(pie) do
p:teleportTo(WaveEvent.RewardRoom)
end
closeEvent()
return true
end
WaveEvent.Arena.monsters[target:getId()] = nil
if countMonstersInEvent() > 0 then
return true
end
local n = Game.getStorageValue(WaveEvent.WAVE_ROUND_STORAGE) or 1
local bcMsg = ordinal_number(n) ..' wave has been cleared.'
if n < WaveEvent.MAX_WAVES then
bcMsg = bcMsg .. ' Prepare for the next wave. ('.. secondsToReadable(WaveEvent.NEXT_WAVE_DELAY / 1000).. ')'
else
bcMsg = bcMsg .. ' Prepare for the final boss!'
end
local playersInEvent = getPlayersInEvent()
for _, player in pairs(playersInEvent) do
player:say('+EXP', TALKTYPE_MONSTER_SAY)
player:addExperience(4200 * player:getLevel() * WaveEvent.EXP_REWARD_FACTOR) -- meh
end
Game.broadcastMessage(bcMsg)
Game.setStorageValue(WaveEvent.WAVE_ROUND_STORAGE, n + 1)
addEvent(initWave, WaveEvent.NEXT_WAVE_DELAY, n + 1, #playersInEvent)
return true
end
csx:register()
local csy = CreatureEvent('WaveEventPHC')
function csy.onHealthChange(player, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin)
if (primaryType == COMBAT_HEALING)
or (not isInEvent(player))
or (player:getStorageValue(WaveEvent.DMG_OVERFLOW) == 1) then
player:setStorageValue(WaveEvent.DMG_OVERFLOW, -1)
return primaryDamage, primaryType, secondaryDamage, secondaryType
end
return primaryDamage * WaveEvent.MONSTERS.DMG_INCREMENT,
primaryType,
secondaryDamage * WaveEvent.MONSTERS.DMG_INCREMENT,
secondaryType
end
csy:register()
local csz = CreatureEvent('WaveEventPMC')
function csz.onManaChange(player, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType, origin)
if (primaryType == COMBAT_HEALING) or (not isInEvent(player)) then
return primaryDamage, primaryType, secondaryDamage, secondaryType
end
local dmgTotal = primaryDamage + secondaryDamage
if dmgTotal > player:getMana() then
player:setStorageValue(WaveEvent.DMG_OVERFLOW, 1)
end
return primaryDamage * WaveEvent.MONSTERS.DMG_INCREMENT,
primaryType,
secondaryDamage * WaveEvent.MONSTERS.DMG_INCREMENT,
secondaryType
end
csz:register()
--------------------------------------------------------------------------------
------------------------------------ # ACTION # --------------------------------
--------------------------------------------------------------------------------
local action = Action()
function action.onUse(player, item, fromPosition, itemEx, toPosition, isHotkey)
local hasUsed = player:getStorageValue(WaveEvent.REWARD_CHEST_USED)
if hasUsed == 1 then
player:sendTextMessage(MESSAGE_STATUS_SMALL, 'You have already obtained the reward.')
player:getPosition():sendMagicEffect(CONST_ME_POFF)
return true
end
local diff = Game.getStorageValue(WaveEvent.REWARD_DIFFICULTY)
local rewards = WaveEvent.REWARDS[diff]
local items = ''
for _, rewardItem in pairs(rewards) do
local chance = rewardItem[3]
if math.random(1, 100) <= chance then
items = items .. (#items > 0 and ', ' or '')
local itemId = rewardItem[1]
local count = rewardItem[2]
local itemX = Game.createItem(itemId, count)
local depot = player:getDepotChest(player:getTown():getId(), true)
depot:addItemEx(itemX, INDEX_WHEREEVER, FLAG_NOLIMIT)
items = items .. '('.. count ..'x ' .. itemX:getName() .. (count > 1 and 's' or '') .. ')'
end
end
items = items .. '.'
player:setStorageValue(WaveEvent.REWARD_CHEST_USED, 1)
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, '[WaveEvent] You have received ' .. items)
player:teleportTo(player:getTown():getTemplePosition())
return true
end
action:aid(WaveEvent.REWARD_CHEST_AID)
action:register()