diff --git a/.gitignore b/.gitignore
index aae56bd35a9..d233ae17409 100644
--- a/.gitignore
+++ b/.gitignore
@@ -372,12 +372,15 @@ config.lua
config_canary.lua
client_assertions.txt
.env
+otservbr.otbm
+canary.otbm
+otservbr-custom.otbm
+
# Extensions
*.ini
*.otb
*.exe
*.manifest
-*.otbm
*.rar
*-house.xml
*-monster.xml
diff --git a/data-canary/scripts/actions/other/spellbook.lua b/data-canary/scripts/actions/other/spellbook.lua
deleted file mode 100644
index 600f60a02e9..00000000000
--- a/data-canary/scripts/actions/other/spellbook.lua
+++ /dev/null
@@ -1,38 +0,0 @@
-local spellbook = Action()
-
-function spellbook.onUse(player, item, fromPosition, target, toPosition, isHotkey)
- local text = {}
- local spells = {}
- for _, spell in ipairs(player:getInstantSpells()) do
- if spell.level ~= 0 then
- if spell.manapercent > 0 then
- spell.mana = spell.manapercent .. "%"
- end
- spells[#spells + 1] = spell
- end
- end
-
- table.sort(spells, function(a, b)
- return a.level < b.level
- end)
-
- local prevLevel = -1
- for i, spell in ipairs(spells) do
- if prevLevel ~= spell.level then
- if i == 1 then
- text[#text == nil and 1 or #text + 1] = "Spells for Level "
- else
- text[#text + 1] = "\nSpells for Level "
- end
- text[#text + 1] = spell.level .. "\n"
- prevLevel = spell.level
- end
- text[#text + 1] = spell.words .. " - " .. spell.name .. " : " .. spell.mana .. "\n"
- end
-
- player:showTextDialog(item:getId(), table.concat(text))
- return true
-end
-
-spellbook:id(3059, 6120, 8900, 8901, 8902, 8903, 8904, 8918, 14769, 16107, 20088, 20089, 20090, 21400)
-spellbook:register()
diff --git a/data-otservbr-global/raids/darashia/tyrn.xml b/data-otservbr-global/raids/darashia/tyrn.xml
deleted file mode 100644
index 21b39dda549..00000000000
--- a/data-otservbr-global/raids/darashia/tyrn.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/data-otservbr-global/raids/roshamuul/mawhawk.xml b/data-otservbr-global/raids/roshamuul/mawhawk.xml
deleted file mode 100644
index db48081d57a..00000000000
--- a/data-otservbr-global/raids/roshamuul/mawhawk.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/data-otservbr-global/scripts/actions/other/door_shive1.lua b/data-otservbr-global/scripts/actions/other/door_shive1.lua
deleted file mode 100644
index 2e0baa7052b..00000000000
--- a/data-otservbr-global/scripts/actions/other/door_shive1.lua
+++ /dev/null
@@ -1,16 +0,0 @@
-local doorShive1 = Action()
-
-function doorShive1.onUse(player, item, fromPosition, target, toPosition, isHotkey)
- local position = player:getPosition()
- if position.y == toPosition.y then
- return false
- end
-
- toPosition.y = position.y > toPosition.y and toPosition.y - 1 or toPosition.y + 1
- player:teleportTo(toPosition)
- toPosition:sendMagicEffect(CONST_ME_TELEPORT)
- return true
-end
-
-doorShive1:id(13278, 13279, 13280, 13281, 13282, 13283)
-doorShive1:register()
diff --git a/data-otservbr-global/scripts/actions/other/door_shive2.lua b/data-otservbr-global/scripts/actions/other/door_shive2.lua
deleted file mode 100644
index f1131295acc..00000000000
--- a/data-otservbr-global/scripts/actions/other/door_shive2.lua
+++ /dev/null
@@ -1,16 +0,0 @@
-local doorShive2 = Action()
-
-function doorShive2.onUse(player, item, fromPosition, target, toPosition, isHotkey)
- local position = player:getPosition()
- if position.x == toPosition.x then
- return false
- end
-
- toPosition.x = position.x > toPosition.x and toPosition.x - 1 or toPosition.x + 1
- player:teleportTo(toPosition)
- toPosition:sendMagicEffect(CONST_ME_TELEPORT)
- return true
-end
-
-doorShive2:id(13290, 13291, 13292, 13293, 13294)
-doorShive2:register()
diff --git a/data-otservbr-global/scripts/actions/spellbook.lua b/data-otservbr-global/scripts/actions/spellbook.lua
deleted file mode 100644
index 2e1328bcac4..00000000000
--- a/data-otservbr-global/scripts/actions/spellbook.lua
+++ /dev/null
@@ -1,58 +0,0 @@
-local spellbook = Action()
-
-function spellbook.onUse(player, item, fromPosition, target, toPosition, isHotkey)
- local text = ""
- local tlvl = {}
- local tml = {}
-
- for _, spell in ipairs(player:getInstantSpells()) do
- if spell.level ~= 0 or spell.mlevel ~= 0 then
- if spell.manapercent > 0 then
- spell.mana = spell.manapercent .. "%"
- end
- if spell.level > 0 then
- tlvl[#tlvl + 1] = spell
- elseif spell.mlevel > 0 then
- tml[#tml + 1] = spell
- end
- end
- end
-
- table.sort(tlvl, function(a, b)
- return a.level < b.level
- end)
- local prevLevel = -1
- for i, spell in ipairs(tlvl) do
- local line = ""
- if prevLevel ~= spell.level then
- if i ~= 1 then
- line = "\n"
- end
- line = line .. "Spells for Level " .. spell.level .. "\n"
- prevLevel = spell.level
- end
- text = text .. line .. " " .. spell.words .. " - " .. spell.name .. " : " .. spell.mana .. "\n"
- end
- text = text .. "\n"
- table.sort(tml, function(a, b)
- return a.mlevel < b.mlevel
- end)
- local prevmLevel = -1
- for i, spell in ipairs(tml) do
- local line = ""
- if prevLevel ~= spell.mlevel then
- if i ~= 1 then
- line = "\n"
- end
- line = line .. "Spells for Magic Level " .. spell.mlevel .. "\n"
- prevmLevel = spell.mlevel
- end
- text = text .. line .. " " .. spell.words .. " - " .. spell.name .. " : " .. spell.mana .. "\n"
- end
-
- player:showTextDialog(item:getId(), text)
- return true
-end
-
-spellbook:id(3059, 6120, 8072, 8073, 8074, 8075, 8076, 8090, 11691, 14769, 16107, 20088, 21400, 22755, 25699, 29431, 20089, 20090, 34153)
-spellbook:register()
diff --git a/data-otservbr-global/scripts/globalevents/others/hireling_save.lua b/data-otservbr-global/scripts/globalevents/others/hireling_save.lua
deleted file mode 100644
index 19a3ba364f2..00000000000
--- a/data-otservbr-global/scripts/globalevents/others/hireling_save.lua
+++ /dev/null
@@ -1,8 +0,0 @@
-local hirelingSave = GlobalEvent("hirelingSave")
-function hirelingSave.onShutdown()
- logger.info("Saving Hirelings")
- SaveHirelings()
- return true
-end
-
-hirelingSave:register()
diff --git a/data-otservbr-global/scripts/globalevents/others/raids_schedule.lua b/data-otservbr-global/scripts/globalevents/others/raids_schedule.lua
new file mode 100644
index 00000000000..be6070d857a
--- /dev/null
+++ b/data-otservbr-global/scripts/globalevents/others/raids_schedule.lua
@@ -0,0 +1,53 @@
+local raidSchedule = {
+ ["Tuesday"] = {
+ ["16:00"] = { raidName = "Midnight Panther" },
+ },
+ ["Wednesday"] = {
+ ["12:00"] = { raidName = "Draptor" },
+ },
+ ["Thursday"] = {
+ ["19:00"] = { raidName = "Undead Cavebear" },
+ },
+ ["Friday"] = {
+ ["06:00"] = { raidName = "Titanica" },
+ },
+ ["Saturday"] = {
+ ["20:00"] = { raidName = "Draptor" },
+ },
+ ["Sunday"] = {
+ ["15:00"] = { raidName = "Midnight Panther" },
+ ["13:00"] = { raidName = "Orc Backpack" },
+ },
+ ["31/10"] = {
+ ["16:00"] = { raidName = "Halloween Hare" },
+ },
+}
+
+local spawnRaidsEvent = GlobalEvent("SpawnRaidsEvent")
+
+function spawnRaidsEvent.onThink(interval, lastExecution, thinkInterval)
+ local currentDayOfWeek, currentDate = os.date("%A"), getRealDate()
+ local raidsToSpawn = {}
+
+ if raidSchedule[currentDayOfWeek] then
+ raidsToSpawn[#raidsToSpawn + 1] = raidSchedule[currentDayOfWeek]
+ end
+
+ if raidSchedule[currentDate] then
+ raidsToSpawn[#raidsToSpawn + 1] = raidSchedule[currentDate]
+ end
+
+ if #raidsToSpawn > 0 then
+ for i = 1, #raidsToSpawn do
+ local currentRaidSchedule = raidsToSpawn[i][getRealTime()]
+ if currentRaidSchedule and not currentRaidSchedule.alreadyExecuted then
+ Game.startRaid(currentRaidSchedule.raidName)
+ currentRaidSchedule.alreadyExecuted = true
+ end
+ end
+ end
+ return true
+end
+
+spawnRaidsEvent:interval(60000)
+spawnRaidsEvent:register()
diff --git a/data-otservbr-global/scripts/globalevents/spawn/grimvale_respawn.lua b/data-otservbr-global/scripts/globalevents/spawn/grimvale_respawn.lua
deleted file mode 100644
index aff3f6e3fa0..00000000000
--- a/data-otservbr-global/scripts/globalevents/spawn/grimvale_respawn.lua
+++ /dev/null
@@ -1,59 +0,0 @@
-local config = {
- position = { fromPosition = Position(33330, 31670, 7), toPosition = Position(33350, 31690, 7) },
-}
-local spawnDay = 13
-local currentDay = os.date("%d")
-local monsters = {}
-
-function Game.createRandom(position)
- local tile = Tile(position)
- if not tile or Tile(position):getItemById(486) then
- return false
- end
-
- local ground = tile:getGround()
- if not ground or ground:hasProperty(CONST_PROP_BLOCKSOLID) or tile:getTopCreature() then
- return false
- end
- local monsterName = monsters[math.random(#monsters)]
- local monster = Game.createMonster(monsterName, position)
- if monster then
- monster:setSpawnPosition()
- monster:remove()
- end
- return true
-end
-
-local grimvaleRespawn = GlobalEvent("grimvale respawn")
-function grimvaleRespawn.onStartup()
- if spawnDay == tonumber(currentDay) then
- table.insert(monsters, "wereboar")
- table.insert(monsters, "werebadger")
- for x = config.position.fromPosition.x, config.position.toPosition.x do
- for y = config.position.fromPosition.y, config.position.toPosition.y do
- if math.random(1000) >= 983 then
- if Game.createRandom(Position(x, y, 7)) then
- return
- end
- end
- end
- end
- else
- table.insert(monsters, "bandit")
- table.insert(monsters, "badger")
- table.insert(monsters, "blue butterfly")
- table.insert(monsters, "yellow butterfly")
- for x = config.position.fromPosition.x, config.position.toPosition.x do
- for y = config.position.fromPosition.y, config.position.toPosition.y do
- if math.random(1000) >= 983 then
- if Game.createRandom(Position(x, y, 7)) then
- return
- end
- end
- end
- end
- end
- return true
-end
-
-grimvaleRespawn:register()
diff --git a/data-otservbr-global/scripts/globalevents/spawn/mawhawk.lua b/data-otservbr-global/scripts/globalevents/spawn/mawhawk.lua
deleted file mode 100644
index e34657d211c..00000000000
--- a/data-otservbr-global/scripts/globalevents/spawn/mawhawk.lua
+++ /dev/null
@@ -1,18 +0,0 @@
-local config = {
- monsterName = "Mawhawk",
- bossPosition = Position(33703, 32461, 7),
- centerPosition = Position(33703, 32461, 7),
- rangeX = 50,
- rangeY = 50,
-}
-
-local mawhawk = GlobalEvent("mawhawk")
-function mawhawk.onThink(interval, lastExecution)
- if not checkBoss(config.centerPosition, config.rangeX, config.rangeY, config.monsterName, config.bossPosition) then
- addEvent(Game.broadcastMessage, 150, "Beware! Mawhawk!", MESSAGE_EVENT_ADVANCE)
- end
- return true
-end
-
-mawhawk:interval(10 * 60 * 60 * 1000) -- spawns every 10 hours
-mawhawk:register()
diff --git a/data-otservbr-global/scripts/globalevents/spawn/raids.lua b/data-otservbr-global/scripts/globalevents/spawn/raids.lua
deleted file mode 100644
index 83d065349dd..00000000000
--- a/data-otservbr-global/scripts/globalevents/spawn/raids.lua
+++ /dev/null
@@ -1,65 +0,0 @@
-local raids = {
- --Terça-Feira
- ["Tuesday"] = {
- ["16:00"] = { raidName = "Midnight Panther" },
- },
-
- --Quarta-Feira
- ["Wednesday"] = {
- ["12:00"] = { raidName = "Draptor" },
- },
-
- --Quinta-Feira
- ["Thursday"] = {
- ["19:00"] = { raidName = "Undead Cavebear" },
- },
-
- --Sexta-feira
- ["Friday"] = {
- ["06:00"] = { raidName = "Titanica" },
- },
-
- --Sábado
- ["Saturday"] = {
- ["20:00"] = { raidName = "Draptor" },
- },
-
- --Domingo
- ["Sunday"] = {
- ["15:00"] = { raidName = "Midnight Panther" },
- ["13:00"] = { raidName = "Orc Backpack" },
- },
-
- -- By date (Day/Month)
- ["31/10"] = {
- ["16:00"] = { raidName = "Halloween Hare" },
- },
-}
-
-local spawnRaids = GlobalEvent("spawn raids")
-function spawnRaids.onThink(interval, lastExecution, thinkInterval)
- local day, date = os.date("%A"), getRealDate()
-
- local raidDays = {}
- if raids[day] then
- raidDays[#raidDays + 1] = raids[day]
- end
- if raids[date] then
- raidDays[#raidDays + 1] = raids[date]
- end
- if #raidDays == 0 then
- return true
- end
-
- for i = 1, #raidDays do
- local settings = raidDays[i][getRealTime()]
- if settings and not settings.alreadyExecuted then
- Game.startRaid(settings.raidName)
- settings.alreadyExecuted = true
- end
- end
- return true
-end
-
-spawnRaids:interval(60000)
-spawnRaids:register()
diff --git a/data-otservbr-global/scripts/globalevents/spawn/thawing_dragon_lord.lua b/data-otservbr-global/scripts/globalevents/spawn/thawing_dragon_lord.lua
deleted file mode 100644
index eecbd29022b..00000000000
--- a/data-otservbr-global/scripts/globalevents/spawn/thawing_dragon_lord.lua
+++ /dev/null
@@ -1,17 +0,0 @@
-local config = {
- monsterName = "Thawing Dragon Lord",
- bossPosition = Position(33361, 31316, 5),
- centerPosition = Position(33361, 31316, 5),
- rangeX = 50,
- rangeY = 50,
-}
-
-local thawingDragonLord = GlobalEvent("thawing dragon lord")
-
-function thawingDragonLord.onThink(interval, lastExecution)
- checkBoss(config.centerPosition, config.rangeX, config.rangeY, config.monsterName, config.bossPosition)
- return true
-end
-
-thawingDragonLord:interval(900000)
-thawingDragonLord:register()
diff --git a/data-otservbr-global/scripts/globalevents/spawn/tyrn.lua b/data-otservbr-global/scripts/globalevents/spawn/tyrn.lua
deleted file mode 100644
index 47e7756f2ba..00000000000
--- a/data-otservbr-global/scripts/globalevents/spawn/tyrn.lua
+++ /dev/null
@@ -1,18 +0,0 @@
-local config = {
- monsterName = "Tyrn",
- bossPosition = Position(33056, 32393, 14),
- centerPosition = Position(33056, 32393, 14),
- rangeX = 50,
- rangeY = 50,
-}
-
-local tyrn = GlobalEvent("tyrn")
-function tyrn.onThink(interval, lastExecution)
- if not checkBoss(config.centerPosition, config.rangeX, config.rangeY, config.monsterName, config.bossPosition) then
- addEvent(Game.broadcastMessage, 150, "Beware of Tyrn!", MESSAGE_EVENT_ADVANCE)
- end
- return true
-end
-
-tyrn:interval(9 * 60 * 60 * 1000) -- spawns every 9 hours
-tyrn:register()
diff --git a/data-otservbr-global/scripts/globalevents/worldchanges/dream_courts_worldchange.lua b/data-otservbr-global/scripts/globalevents/worldchanges/dream_courts_worldchange.lua
deleted file mode 100644
index 090887b9540..00000000000
--- a/data-otservbr-global/scripts/globalevents/worldchanges/dream_courts_worldchange.lua
+++ /dev/null
@@ -1,21 +0,0 @@
-local config = {
- ["Monday"] = "Plagueroot",
- ["Tuesday"] = "Malofur_Mangrinder",
- ["Wednesday"] = "Maxxenius",
- ["Thursday"] = "Alptramun",
- ["Friday"] = "Izcandar_the_Banished",
- ["Saturday"] = "Maxxenius",
- ["Sunday"] = "Alptramun",
-}
-local spawnByDay = true
-
-local DreamCourts = GlobalEvent("DreamCourts")
-function DreamCourts.onStartup()
- if spawnByDay then
- logger.info("[WorldChanges] Dream Courts loaded: {}.otbm", config[os.date("%A")])
- Game.loadMap("data-otservbr-global/world/world_changes/dream_courts_bosses/" .. config[os.date("%A")] .. ".otbm")
- end
- return true
-end
-
-DreamCourts:register()
diff --git a/data-otservbr-global/scripts/globalevents/worldchanges/their_masters_voice.lua b/data-otservbr-global/scripts/globalevents/worldchanges/their_masters_voice.lua
deleted file mode 100644
index 2061c254cf9..00000000000
--- a/data-otservbr-global/scripts/globalevents/worldchanges/their_masters_voice.lua
+++ /dev/null
@@ -1,31 +0,0 @@
-local theirmastersvoice = GlobalEvent("theirmastersvoice")
-function theirmastersvoice.onStartup()
- local eventEnabled = true
- local eventChance = 20
- local function fillFungus(fromPosition, toPosition)
- for x = fromPosition.x, toPosition.x do
- for y = fromPosition.y, toPosition.y do
- local position = Position(x, y, 9)
- local tile = Tile(position)
- if tile then
- local item = tile:getItemById(12065)
- if item then
- local slimeChance = math.random(100)
- if slimeChance <= 30 then
- item:transform(math.random(12059, 12063))
- position:sendMagicEffect(CONST_ME_YELLOW_RINGS)
- end
- end
- end
- end
- end
- end
-
- if eventEnabled then
- if math.random(100) <= eventChance then
- fillFungus({ x = 33306, y = 31847 }, { x = 33369, y = 31919 })
- end
- end
-end
-
-theirmastersvoice:register()
diff --git a/data-otservbr-global/scripts/world_changes/grimvale_respawn_event.lua b/data-otservbr-global/scripts/world_changes/grimvale_respawn_event.lua
new file mode 100644
index 00000000000..b1cea5504e9
--- /dev/null
+++ b/data-otservbr-global/scripts/world_changes/grimvale_respawn_event.lua
@@ -0,0 +1,40 @@
+local grimvaleConfig = {
+ position = { fromPosition = Position(33330, 31670, 7), toPosition = Position(33350, 31690, 7) },
+ spawnDay = 13,
+}
+
+local function createRandomMonster(position, availableMonsters)
+ local tile = Tile(position)
+ if not tile or tile:getItemById(486) or tile:hasProperty(CONST_PROP_BLOCKSOLID) or tile:getTopCreature() then
+ return false
+ end
+
+ local monsterName = availableMonsters[math.random(#availableMonsters)]
+ local monster = Game.createMonster(monsterName, position)
+ if monster then
+ monster:setSpawnPosition()
+ monster:remove()
+ end
+ return true
+end
+
+local function spawnMonsters(monstersToSpawn)
+ for x = grimvaleConfig.position.fromPosition.x, grimvaleConfig.position.toPosition.x do
+ for y = grimvaleConfig.position.fromPosition.y, grimvaleConfig.position.toPosition.y do
+ if math.random(1000) >= 983 then
+ if createRandomMonster(Position(x, y, 7), monstersToSpawn) then
+ break
+ end
+ end
+ end
+ end
+end
+
+local grimvaleRespawnEvent = GlobalEvent("GrimvaleRespawnEvent")
+
+function grimvaleRespawnEvent.onStartup()
+ spawnMonsters(grimvaleConfig.spawnDay == tonumber(os.date("%d")) and { "wereboar", "werebadger" } or { "bandit", "badger", "blue butterfly", "yellow butterfly" })
+ return true
+end
+
+grimvaleRespawnEvent:register()
diff --git a/data-otservbr-global/scripts/world_changes/the_dream_courts.lua b/data-otservbr-global/scripts/world_changes/the_dream_courts.lua
new file mode 100644
index 00000000000..bb3bcc3a6a8
--- /dev/null
+++ b/data-otservbr-global/scripts/world_changes/the_dream_courts.lua
@@ -0,0 +1,26 @@
+local dreamCourtsConfig = {
+ ["Monday"] = { map = "plagueroot", bossName = "Plagueroot" },
+ ["Tuesday"] = { map = "malofur_mangrinder", bossName = "Malofur Mangrinder" },
+ ["Wednesday"] = { map = "maxxenius", bossName = "Maxxenius" },
+ ["Thursday"] = { map = "alptramun", bossName = "Alptramun" },
+ ["Friday"] = { map = "izcandar_the_banished", bossName = "Izcandar the Banished" },
+ ["Saturday"] = { map = "maxxenius", bossName = "Maxxenius" },
+ ["Sunday"] = { map = "alptramun", bossName = "Alptramun" },
+}
+
+local dreamCourtsEvent = GlobalEvent("DreamCourts")
+
+function dreamCourtsEvent.onStartup()
+ local currentDay = os.date("%A")
+ local dayConfig = dreamCourtsConfig[currentDay]
+ if not dayConfig then
+ logger.warn("[World Change] The Dream Courts map not defined for the current day: {}", currentDay)
+ return false
+ end
+
+ Game.loadMap(DATA_DIRECTORY .. "/world/quest/the_dream_courts/" .. dayConfig.map .. ".otbm")
+ logger.info("[World Change] The Dream Courts today's boss is: {}!", dayConfig.bossName)
+ return true
+end
+
+dreamCourtsEvent:register()
diff --git a/data-otservbr-global/scripts/world_changes/their_masters_voice.lua b/data-otservbr-global/scripts/world_changes/their_masters_voice.lua
new file mode 100644
index 00000000000..af1354dae6c
--- /dev/null
+++ b/data-otservbr-global/scripts/world_changes/their_masters_voice.lua
@@ -0,0 +1,21 @@
+local theirMastersVoiceEvent = GlobalEvent("TheirMastersVoice")
+
+function theirMastersVoiceEvent.onStartup()
+ if math.random(100) <= 20 then
+ for x = 33306, 33369 do
+ for y = 31847, 31919 do
+ local position = Position(x, y, 9)
+ local tile = Tile(position)
+ if tile then
+ local fungus = tile:getItemById(12065)
+ if fungus and math.random(100) <= 30 then
+ fungus:transform(math.random(12059, 12063))
+ fungus:getPosition():sendMagicEffect(CONST_ME_YELLOW_RINGS)
+ end
+ end
+ end
+ end
+ end
+end
+
+theirMastersVoiceEvent:register()
diff --git a/data-otservbr-global/world/world_changes/dream_courts_bosses/Alptramun.otbm b/data-otservbr-global/world/quest/the_dream_courts/alptramun.otbm
similarity index 100%
rename from data-otservbr-global/world/world_changes/dream_courts_bosses/Alptramun.otbm
rename to data-otservbr-global/world/quest/the_dream_courts/alptramun.otbm
diff --git a/data-otservbr-global/world/world_changes/dream_courts_bosses/Izcandar_the_Banished.otbm b/data-otservbr-global/world/quest/the_dream_courts/izcandar_the_banished.otbm
similarity index 100%
rename from data-otservbr-global/world/world_changes/dream_courts_bosses/Izcandar_the_Banished.otbm
rename to data-otservbr-global/world/quest/the_dream_courts/izcandar_the_banished.otbm
diff --git a/data-otservbr-global/world/world_changes/dream_courts_bosses/Malofur_Mangrinder.otbm b/data-otservbr-global/world/quest/the_dream_courts/malofur_mangrinder.otbm
similarity index 100%
rename from data-otservbr-global/world/world_changes/dream_courts_bosses/Malofur_Mangrinder.otbm
rename to data-otservbr-global/world/quest/the_dream_courts/malofur_mangrinder.otbm
diff --git a/data-otservbr-global/world/world_changes/dream_courts_bosses/Maxxenius.otbm b/data-otservbr-global/world/quest/the_dream_courts/maxxenius.otbm
similarity index 100%
rename from data-otservbr-global/world/world_changes/dream_courts_bosses/Maxxenius.otbm
rename to data-otservbr-global/world/quest/the_dream_courts/maxxenius.otbm
diff --git a/data-otservbr-global/world/world_changes/dream_courts_bosses/Plagueroot.otbm b/data-otservbr-global/world/quest/the_dream_courts/plagueroot.otbm
similarity index 100%
rename from data-otservbr-global/world/world_changes/dream_courts_bosses/Plagueroot.otbm
rename to data-otservbr-global/world/quest/the_dream_courts/plagueroot.otbm
diff --git a/data/scripts/actions/items/spellbook.lua b/data/scripts/actions/items/spellbook.lua
new file mode 100644
index 00000000000..ef777b68269
--- /dev/null
+++ b/data/scripts/actions/items/spellbook.lua
@@ -0,0 +1,55 @@
+local function sortSpellsByLevel(spellList, levelKey)
+ table.sort(spellList, function(a, b)
+ return a[levelKey] < b[levelKey]
+ end)
+end
+
+local function appendSpellsInfo(spellList, header, levelKey, manaKey)
+ local text = ""
+ local prevLevel = -1
+
+ for i, spell in ipairs(spellList) do
+ local line = ""
+ if prevLevel ~= spell[levelKey] then
+ line = (i == 1 and "" or "\n") .. header .. spell[levelKey] .. "\n"
+ prevLevel = spell[levelKey]
+ end
+
+ text = text .. line .. " " .. spell.words .. " - " .. spell.name .. " : " .. spell[manaKey] .. "\n"
+ end
+ return text
+end
+
+local spellbook = Action()
+
+function spellbook.onUse(player, item, fromPosition, target, toPosition, isHotkey)
+ local spellsForLevel = {}
+ local spellsForMagicLevel = {}
+
+ for _, spell in ipairs(player:getInstantSpells()) do
+ if (spell.level > 0 or spell.mlevel > 0) and spell.level + spell.mlevel > 0 then
+ if spell.manapercent > 0 then
+ spell.mana = spell.manapercent .. "%"
+ end
+
+ if spell.level > 0 then
+ spellsForLevel[#spellsForLevel + 1] = spell
+ else
+ spellsForMagicLevel[#spellsForMagicLevel + 1] = spell
+ end
+ end
+ end
+
+ sortSpellsByLevel(spellsForLevel, "level")
+ sortSpellsByLevel(spellsForMagicLevel, "mlevel")
+
+ local spellsText = appendSpellsInfo(spellsForLevel, "Spells for Level ", "level", "mana")
+ spellsText = spellsText .. "\n"
+ spellsText = spellsText .. appendSpellsInfo(spellsForMagicLevel, "Spells for Magic Level ", "mlevel", "mana")
+
+ player:showTextDialog(item:getId(), spellsText)
+ return true
+end
+
+spellbook:id(3059, 6120, 8072, 8073, 8074, 8075, 8076, 8090, 11691, 14769, 16107, 20088, 21400, 22755, 25699, 29431, 20089, 20090, 34153)
+spellbook:register()
diff --git a/data/scripts/actions/objects/hive_gates.lua b/data/scripts/actions/objects/hive_gates.lua
new file mode 100644
index 00000000000..12c56b8ce74
--- /dev/null
+++ b/data/scripts/actions/objects/hive_gates.lua
@@ -0,0 +1,25 @@
+local hiveGates = Action()
+
+function hiveGates.onUse(player, item, fromPosition, target, toPosition, isHotkey)
+ local position = player:getPosition()
+ if item:getId() == 13278 or item:getId() == 13279 or item:getId() == 13280 or item:getId() == 13281 or item:getId() == 13282 or item:getId() == 13283 then
+ if position.y == toPosition.y then
+ return false
+ end
+
+ toPosition.y = position.y > toPosition.y and toPosition.y - 1 or toPosition.y + 1
+ elseif item:getId() == 13290 or item:getId() == 13291 or item:getId() == 13292 or item:getId() == 13293 or item:getId() == 13294 then
+ if position.x == toPosition.x then
+ return false
+ end
+
+ toPosition.x = position.x > toPosition.x and toPosition.x - 1 or toPosition.x + 1
+ end
+
+ player:teleportTo(toPosition)
+ toPosition:sendMagicEffect(CONST_ME_TELEPORT)
+ return true
+end
+
+hiveGates:id(13278, 13279, 13280, 13281, 13282, 13283, 13290, 13291, 13292, 13293, 13294)
+hiveGates:register()
diff --git a/data/scripts/creaturescripts/player/send_first_items.lua b/data/scripts/creaturescripts/player/send_first_items.lua
new file mode 100644
index 00000000000..53fecd11554
--- /dev/null
+++ b/data/scripts/creaturescripts/player/send_first_items.lua
@@ -0,0 +1,113 @@
+local config = {
+ [VOCATION.ID.NONE] = {
+ container = {
+ { 3003, 1 }, -- rope
+ { 3457, 1 }, -- shovel
+ },
+ },
+
+ [VOCATION.ID.SORCERER] = {
+ items = {
+ { 3059, 1 }, -- spellbook
+ { 3074, 1 }, -- wand of vortex
+ { 7991, 1 }, -- magician's robe
+ { 7992, 1 }, -- mage hat
+ { 3362, 1 }, -- studded legs
+ { 3552, 1 }, -- leather boots
+ { 3572, 1 }, -- scarf
+ },
+
+ container = {
+ { 3003, 1 }, -- rope
+ { 5710, 1 }, -- light shovel
+ { 268, 10 }, -- mana potion
+ },
+ },
+
+ [VOCATION.ID.DRUID] = {
+ items = {
+ { 3059, 1 }, -- spellbook
+ { 3066, 1 }, -- snakebite rod
+ { 7991, 1 }, -- magician's robe
+ { 7992, 1 }, -- mage hat
+ { 3362, 1 }, -- studded legs
+ { 3552, 1 }, -- leather boots
+ { 3572, 1 }, -- scarf
+ },
+
+ container = {
+ { 3003, 1 }, -- rope
+ { 5710, 1 }, -- light shovel
+ { 268, 10 }, -- mana potion
+ },
+ },
+
+ [VOCATION.ID.PALADIN] = {
+ items = {
+ { 3425, 1 }, -- dwarven shield
+ { 3277, 1 }, -- spear
+ { 3571, 1 }, -- ranger's cloak
+ { 8095, 1 }, -- ranger legs
+ { 3552, 1 }, -- leather boots
+ { 3572, 1 }, -- scarf
+ { 3374, 1 }, -- legion helmet
+ },
+
+ container = {
+ { 3003, 1 }, -- rope
+ { 5710, 1 }, -- light shovel
+ { 266, 10 }, -- health potion
+ { 3350, 1 }, -- bow
+ { 3447, 50 }, -- 50 arrows
+ },
+ },
+
+ [VOCATION.ID.KNIGHT] = {
+ items = {
+ { 3425, 1 }, -- dwarven shield
+ { 7773, 1 }, -- steel axe
+ { 3359, 1 }, -- brass armor
+ { 3354, 1 }, -- brass helmet
+ { 3372, 1 }, -- brass legs
+ { 3552, 1 }, -- leather boots
+ { 3572, 1 }, -- scarf
+ },
+
+ container = {
+ { 7774, 1 }, -- jagged sword
+ { 3327, 1 }, -- daramanian mace
+ { 3003, 1 }, -- rope
+ { 5710, 1 }, -- light shovel
+ { 266, 10 }, -- health potion
+ },
+ },
+}
+
+local sendFirstItems = CreatureEvent("SendFirstItems")
+
+function sendFirstItems.onLogin(player)
+ local targetVocation = config[player:getVocation():getId()]
+ if not targetVocation or player:getLastLoginSaved() ~= 0 then
+ return true
+ end
+
+ if targetVocation.items then
+ for i = 1, #targetVocation.items do
+ player:addItem(targetVocation.items[i][1], targetVocation.items[i][2])
+ end
+ end
+
+ local backpack = player:addItem(2854)
+ if not backpack then
+ return true
+ end
+
+ if targetVocation.container then
+ for i = 1, #targetVocation.container do
+ backpack:addItem(targetVocation.container[i][1], targetVocation.container[i][2])
+ end
+ end
+ return true
+end
+
+sendFirstItems:register()
diff --git a/data/scripts/globalevents/hireling_save.lua b/data/scripts/globalevents/hireling_save.lua
new file mode 100644
index 00000000000..82d5189fe45
--- /dev/null
+++ b/data/scripts/globalevents/hireling_save.lua
@@ -0,0 +1,14 @@
+local hirelingSave = GlobalEvent("HirelingSave")
+
+function hirelingSave.onShutdown()
+ local saved = SaveHirelings()
+ if saved then
+ logger.info("[Server Shutdown] Hirelings successfully saved.")
+ else
+ logger.warn("[Server Shutdown] Failed to save hirelings. Please check the logs for details.")
+ end
+
+ return true
+end
+
+hirelingSave:register()