diff --git a/CMakeLists.txt b/CMakeLists.txt index c0ab25857..a576df159 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,12 @@ endif() # ***************************************************************************** list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) +# Configure build options for compatibility with commodity CPUs +if(NOT MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=x86-64 -mtune=generic -mno-avx -mno-sse4") +endif() + # ***************************************************************************** # Include cmake tools # ***************************************************************************** diff --git a/config.lua.dist b/config.lua.dist index 86050471a..735977dc8 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -344,9 +344,11 @@ Setting this to false may pose risks; if a house is abandoned and contains a lar ]] -- Periods: daily/weekly/monthly/yearly/never -- Base: sqm,rent,sqm+rent +toggleCyclopediaHouseAuction = true +daysToCloseBid = 7 housePriceRentMultiplier = 0.0 housePriceEachSQM = 1000 -houseRentPeriod = "never" +houseRentPeriod = "weekly" houseRentRate = 1.0 houseOwnedByAccount = false houseBuyLevel = 100 diff --git a/data-otxserver/lib/core/storages.lua b/data-otxserver/lib/core/storages.lua index 06982ee7f..61eed2108 100644 --- a/data-otxserver/lib/core/storages.lua +++ b/data-otxserver/lib/core/storages.lua @@ -846,6 +846,7 @@ Storage = { NoblemanSecondAddon = 41308, FormorgarMinesHoistSkeleton = 41309, FormorgarMinesHoistChest = 41310, + PickAmount = 41311, }, }, U8_1 = { -- update 8.1 - Reserved Storages 41351 - 41650 @@ -2766,6 +2767,11 @@ Storage = { }, }, }, + U13_20 = { -- update 13.20 - Reserved Storages 47952 - 47970 + RottenBlood = { + AccessDoor = 47952, + }, + }, }, -- Reserved storage from 63951 - 63999 ThaisExhibition = { diff --git a/data-otxserver/migrations/0.lua b/data-otxserver/migrations/0.lua deleted file mode 100644 index b40962e7c..000000000 --- a/data-otxserver/migrations/0.lua +++ /dev/null @@ -1,14 +0,0 @@ -function onUpdateDatabase() - logger.info("Updating database to version 1 (sample players)") - -- Rook Sample - db.query("UPDATE `players` SET `level` = 2, `vocation` = 0, `health` = 155, `healthmax` = 155, `experience` = 100, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 60, `manamax` = 60, `town_id` = 1, `cap` = 410 WHERE `id` = 1;") - -- Sorcerer Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 1, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 2;") - -- Druid Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 2, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 3;") - -- Paladin Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 3, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 4;") - -- Knight Sample - db.query("UPDATE `players` SET `level` = 8, `vocation` = 4, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 5;") - return true -end diff --git a/data-otxserver/migrations/1.lua b/data-otxserver/migrations/1.lua index 180a8b2ce..1c5888f52 100644 --- a/data-otxserver/migrations/1.lua +++ b/data-otxserver/migrations/1.lua @@ -1,26 +1,13 @@ function onUpdateDatabase() - logger.info("Updating database to version 2 (hireling)") - - db.query([[ - CREATE TABLE IF NOT EXISTS `player_hirelings` ( - `id` INT NOT NULL PRIMARY KEY auto_increment, - `player_id` INT NOT NULL, - `name` varchar(255), - `active` tinyint unsigned NOT NULL DEFAULT '0', - `sex` tinyint unsigned NOT NULL DEFAULT '0', - `posx` int(11) NOT NULL DEFAULT '0', - `posy` int(11) NOT NULL DEFAULT '0', - `posz` int(11) NOT NULL DEFAULT '0', - `lookbody` int(11) NOT NULL DEFAULT '0', - `lookfeet` int(11) NOT NULL DEFAULT '0', - `lookhead` int(11) NOT NULL DEFAULT '0', - `looklegs` int(11) NOT NULL DEFAULT '0', - `looktype` int(11) NOT NULL DEFAULT '136', - - FOREIGN KEY(`player_id`) REFERENCES `players`(`id`) - ON DELETE CASCADE - ) - ]]) - - return true + logger.info("Updating database to version 1 (sample players)") + -- Rook Sample + db.query("UPDATE `players` SET `level` = 2, `vocation` = 0, `health` = 155, `healthmax` = 155, `experience` = 100, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 60, `manamax` = 60, `town_id` = 1, `cap` = 410 WHERE `id` = 1;") + -- Sorcerer Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 1, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 2;") + -- Druid Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 2, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 3;") + -- Paladin Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 3, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 4;") + -- Knight Sample + db.query("UPDATE `players` SET `level` = 8, `vocation` = 4, `health` = 185, `healthmax` = 185, `experience` = 4200, `soul` = 100, `lookbody` = 113, `lookfeet` = 115, `lookhead` = 95, `looklegs` = 39, `looktype` = 129, `mana` = 90, `manamax` = 90, `town_id` = 8, `cap` = 470 WHERE `id` = 5;") end diff --git a/data-otxserver/migrations/10.lua b/data-otxserver/migrations/10.lua index 0285bb0fe..adfa41bb5 100644 --- a/data-otxserver/migrations/10.lua +++ b/data-otxserver/migrations/10.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 11 (Guilds Balance)") + logger.info("Updating database to version 10 (Guilds Balance)") db.query("ALTER TABLE `guilds` ADD `balance` bigint(20) UNSIGNED NOT NULL DEFAULT '0';") - return true end diff --git a/data-otxserver/migrations/11.lua b/data-otxserver/migrations/11.lua index 7f448f94a..a10d3d298 100644 --- a/data-otxserver/migrations/11.lua +++ b/data-otxserver/migrations/11.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 12 (Player get daily reward)") + logger.info("Updating database to version 11 (Player get daily reward)") db.query("ALTER TABLE `players` ADD `isreward` tinyint(1) NOT NULL DEFAULT 1") - return true end diff --git a/data-otxserver/migrations/12.lua b/data-otxserver/migrations/12.lua index 0c6e27bb2..20a2afc45 100644 --- a/data-otxserver/migrations/12.lua +++ b/data-otxserver/migrations/12.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 13 (Boosted Creature Outfit)") + logger.info("Updating database to version 12 (Boosted Creature Outfit)") db.query("ALTER TABLE boosted_creature ADD `looktype` int(11) NOT NULL DEFAULT 136;") db.query("ALTER TABLE boosted_creature ADD `lookfeet` int(11) NOT NULL DEFAULT 0;") db.query("ALTER TABLE boosted_creature ADD `looklegs` int(11) NOT NULL DEFAULT 0;") @@ -7,5 +7,4 @@ function onUpdateDatabase() db.query("ALTER TABLE boosted_creature ADD `lookbody` int(11) NOT NULL DEFAULT 0;") db.query("ALTER TABLE boosted_creature ADD `lookaddons` int(11) NOT NULL DEFAULT 0;") db.query("ALTER TABLE boosted_creature ADD `lookmount` int(11) DEFAULT 0;") - return true end diff --git a/data-otxserver/migrations/13.lua b/data-otxserver/migrations/13.lua index 5918b6cbd..4747efdcb 100644 --- a/data-otxserver/migrations/13.lua +++ b/data-otxserver/migrations/13.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 14 (Fixed mana spent)") + logger.info("Updating database to version 13 (Fixed mana spent)") db.query("ALTER TABLE `players` CHANGE `manaspent` `manaspent` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") - return true end diff --git a/data-otxserver/migrations/14.lua b/data-otxserver/migrations/14.lua index d2a1faf27..281eb4072 100644 --- a/data-otxserver/migrations/14.lua +++ b/data-otxserver/migrations/14.lua @@ -1,6 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 15 (Magic Shield Spell)") + logger.info("Updating database to version 14 (Magic Shield Spell)") db.query("ALTER TABLE `players` ADD `manashield` SMALLINT UNSIGNED NOT NULL DEFAULT '0' AFTER `skill_manaleech_amount`") db.query("ALTER TABLE `players` ADD `max_manashield` SMALLINT UNSIGNED NOT NULL DEFAULT '0' AFTER `manashield`") - return true end diff --git a/data-otxserver/migrations/15.lua b/data-otxserver/migrations/15.lua index 1521e9610..2c4a37ba3 100644 --- a/data-otxserver/migrations/15.lua +++ b/data-otxserver/migrations/15.lua @@ -1,8 +1,7 @@ function onUpdateDatabase() - logger.info("Updating database to version 16 (Rook sample and GOD player values)") + logger.info("Updating database to version 15 (Rook sample and GOD player values)") -- Rook Sample db.query("UPDATE `players` SET `maglevel` = 2, `manaspent` = 5936, `skill_club` = 12, `skill_club_tries` = 155, `skill_sword` = 12, `skill_sword_tries` = 155, `skill_axe` = 12, `skill_axe_tries` = 155, `skill_dist` = 12, `skill_dist_tries` = 93 WHERE `id` = 1;") -- GOD db.query("UPDATE `players` SET `health` = 155, `healthmax` = 155, `experience` = 100, `looktype` = 75, `town_id` = 8 WHERE `id` = 6;") - return true end diff --git a/data-otxserver/migrations/16.lua b/data-otxserver/migrations/16.lua index c9ca340f0..027f2fe98 100644 --- a/data-otxserver/migrations/16.lua +++ b/data-otxserver/migrations/16.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - print("Updating database to version 17 (Tutorial support)") + print("Updating database to version 16 (Tutorial support)") db.query("ALTER TABLE `players` ADD `istutorial` SMALLINT(1) NOT NULL DEFAULT '0'") - return true -- true = There are others migrations file | false = this is the last migration file end diff --git a/data-otxserver/migrations/17.lua b/data-otxserver/migrations/17.lua index d25e4ccd5..c84d6ec8e 100644 --- a/data-otxserver/migrations/17.lua +++ b/data-otxserver/migrations/17.lua @@ -1,6 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 18 (Fix guild creation myaac)") + logger.info("Updating database to version 17 (Fix guild creation myaac)") db.query("ALTER TABLE `guilds` ADD `level` int(11) NOT NULL DEFAULT 1") db.query("ALTER TABLE `guilds` ADD `points` int(11) NOT NULL DEFAULT 0") - return true end diff --git a/data-otxserver/migrations/18.lua b/data-otxserver/migrations/18.lua index 01ef60480..0f5777a26 100644 --- a/data-otxserver/migrations/18.lua +++ b/data-otxserver/migrations/18.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 19 (Prey system rework + Task hunting system)") + logger.info("Updating database to version 18 (Prey system rework + Task hunting system)") db.query([[ ALTER TABLE `players` DROP `prey_stamina_1`, @@ -48,6 +48,4 @@ function onUpdateDatabase() `monster_list` BLOB NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - - return true end diff --git a/data-otxserver/migrations/19.lua b/data-otxserver/migrations/19.lua index 382273dfe..dd0c82d07 100644 --- a/data-otxserver/migrations/19.lua +++ b/data-otxserver/migrations/19.lua @@ -1,9 +1,7 @@ function onUpdateDatabase() - logger.info("Updating database to version 20 (Gamestore accepting Tournament Coins)") + logger.info("Updating database to version 19 (Gamestore accepting Tournament Coins)") db.query("ALTER TABLE `accounts` ADD `tournament_coins` int(11) NOT NULL DEFAULT 0 AFTER `coins`") db.query("ALTER TABLE `store_history` ADD `coin_type` tinyint(1) NOT NULL DEFAULT 0 AFTER `description`") db.query("ALTER TABLE `store_history` DROP COLUMN `coins`") -- Not in use anywhere. - - return true end diff --git a/data-otxserver/migrations/2.lua b/data-otxserver/migrations/2.lua index e953d579a..b5674feca 100644 --- a/data-otxserver/migrations/2.lua +++ b/data-otxserver/migrations/2.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 3 (account refactor)") + logger.info("Updating database to version 2 (account refactor)") db.query([[ LOCK TABLES @@ -109,6 +109,4 @@ function onUpdateDatabase() ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - - return true end diff --git a/data-otxserver/migrations/20.lua b/data-otxserver/migrations/20.lua index 49bc9ecd5..f232bf2a7 100644 --- a/data-otxserver/migrations/20.lua +++ b/data-otxserver/migrations/20.lua @@ -1,6 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 21 (Fix market price size)") + logger.info("Updating database to version 20 (Fix market price size)") db.query("ALTER TABLE `market_history` CHANGE `price` `price` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `market_offers` CHANGE `price` `price` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0';") - return true end diff --git a/data-otxserver/migrations/21.lua b/data-otxserver/migrations/21.lua index a2e945576..5c9e1a0ec 100644 --- a/data-otxserver/migrations/21.lua +++ b/data-otxserver/migrations/21.lua @@ -1,8 +1,7 @@ function onUpdateDatabase() - logger.info("Updating database to version 22 (forge and tier system)") + logger.info("Updating database to version 21 (forge and tier system)") db.query("ALTER TABLE `market_offers` ADD `tier` tinyint UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `market_history` ADD `tier` tinyint UNSIGNED NOT NULL DEFAULT '0';") db.query("ALTER TABLE `players` ADD `forge_dusts` bigint(21) NOT NULL DEFAULT '0';") db.query("ALTER TABLE `players` ADD `forge_dust_level` bigint(21) UNSIGNED NOT NULL DEFAULT '100';") - return true end diff --git a/data-otxserver/migrations/22.lua b/data-otxserver/migrations/22.lua index ee1caf87a..9a2a4475a 100644 --- a/data-otxserver/migrations/22.lua +++ b/data-otxserver/migrations/22.lua @@ -1,8 +1,7 @@ function onUpdateDatabase() - logger.info("Updating database to version 23 (fix offline training skill size)") + logger.info("Updating database to version 22 (fix offline training skill size)") db.query([[ ALTER TABLE `players` MODIFY offlinetraining_skill tinyint(2) NOT NULL DEFAULT '-1'; ]]) - return true end diff --git a/data-otxserver/migrations/23.lua b/data-otxserver/migrations/23.lua index da05835f7..dbf161bb4 100644 --- a/data-otxserver/migrations/23.lua +++ b/data-otxserver/migrations/23.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 24 (forge history)") + logger.info("Updating database to version 23 (forge history)") db.query([[ CREATE TABLE IF NOT EXISTS `forge_history` ( `id` int NOT NULL AUTO_INCREMENT, @@ -16,5 +16,4 @@ function onUpdateDatabase() FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - return true end diff --git a/data-otxserver/migrations/24.lua b/data-otxserver/migrations/24.lua index c3cc1563b..2d5286e56 100644 --- a/data-otxserver/migrations/24.lua +++ b/data-otxserver/migrations/24.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 25 (random mount outfit window)") + logger.info("Updating database to version 24 (random mount outfit window)") db.query("ALTER TABLE `players` ADD `randomize_mount` SMALLINT(1) NOT NULL DEFAULT '0'") - return true end diff --git a/data-otxserver/migrations/25.lua b/data-otxserver/migrations/25.lua index e486b9ddf..41f83e2b7 100644 --- a/data-otxserver/migrations/25.lua +++ b/data-otxserver/migrations/25.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 26 (reward bag fix)") + logger.info("Updating database to version 25 (reward bag fix)") db.query("UPDATE player_rewards SET pid = 0 WHERE itemtype = 19202;") - return true end diff --git a/data-otxserver/migrations/26.lua b/data-otxserver/migrations/26.lua index bbac31b81..24db96633 100644 --- a/data-otxserver/migrations/26.lua +++ b/data-otxserver/migrations/26.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 27 (towns)") + logger.info("Updating database to version 26 (towns)") db.query([[ CREATE TABLE IF NOT EXISTS `towns` ( @@ -11,5 +11,4 @@ function onUpdateDatabase() PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`)) ]]) - return true end diff --git a/data-otxserver/migrations/27.lua b/data-otxserver/migrations/27.lua index b16b589da..478e6da62 100644 --- a/data-otxserver/migrations/27.lua +++ b/data-otxserver/migrations/27.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 28 (bosstiary system)") + logger.info("Updating database to version 27 (bosstiary system)") db.query("ALTER TABLE `players` ADD `boss_points` int NOT NULL DEFAULT '0';") db.query([[ CREATE TABLE IF NOT EXISTS `boosted_boss` ( @@ -23,5 +23,4 @@ function onUpdateDatabase() `bossIdSlotTwo` int NOT NULL DEFAULT 0, `removeTimes` int NOT NULL DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;]]) - return true end diff --git a/data-otxserver/migrations/28.lua b/data-otxserver/migrations/28.lua index 2bd44799c..06adece4d 100644 --- a/data-otxserver/migrations/28.lua +++ b/data-otxserver/migrations/28.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 29 (transfer coins)") + logger.info("Updating database to version 28 (transfer coins)") db.query("ALTER TABLE `accounts` ADD `coins_transferable` int unsigned NOT NULL DEFAULT '0';") - return true end diff --git a/data-otxserver/migrations/29.lua b/data-otxserver/migrations/29.lua index 1b490d324..91834f4f2 100644 --- a/data-otxserver/migrations/29.lua +++ b/data-otxserver/migrations/29.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 30 (looktypeEx)") + logger.info("Updating database to version 29 (looktypeEx)") db.query("ALTER TABLE `boosted_boss` ADD `looktypeEx` int unsigned NOT NULL DEFAULT '0';") - return true end diff --git a/data-otxserver/migrations/3.lua b/data-otxserver/migrations/3.lua index 72fc0d41c..0a1aec6f6 100644 --- a/data-otxserver/migrations/3.lua +++ b/data-otxserver/migrations/3.lua @@ -1,9 +1,8 @@ function onUpdateDatabase() - logger.info("Updating database to version 4 (prey tick)") + logger.info("Updating database to version 3 (prey tick)") db.query([[ ALTER TABLE `prey_slots` ADD `tick` smallint(3) NOT NULL DEFAULT '0'; ]]) - return true end diff --git a/data-otxserver/migrations/30.lua b/data-otxserver/migrations/30.lua index f724284e2..4749f1588 100644 --- a/data-otxserver/migrations/30.lua +++ b/data-otxserver/migrations/30.lua @@ -1,7 +1,6 @@ function onUpdateDatabase() - logger.info("Updating database to version 31 (loyalty)") + logger.info("Updating database to version 30 (loyalty)") db.query([[ ALTER TABLE `accounts` ADD COLUMN `premdays_purchased` int(11) NOT NULL DEFAULT 0; ]]) - return true end diff --git a/data-otxserver/migrations/31.lua b/data-otxserver/migrations/31.lua index 4207776b9..9659f296a 100644 --- a/data-otxserver/migrations/31.lua +++ b/data-otxserver/migrations/31.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 32 (account_sessions)") + logger.info("Updating database to version 31 (account_sessions)") db.query([[ CREATE TABLE IF NOT EXISTS `account_sessions` ( `id` VARCHAR(191) NOT NULL, @@ -13,5 +13,4 @@ function onUpdateDatabase() db.query([[ ALTER TABLE `accounts` MODIFY `password` TEXT NOT NULL; ]]) - return true end diff --git a/data-otxserver/migrations/32.lua b/data-otxserver/migrations/32.lua index 102a9aafd..c90de6188 100644 --- a/data-otxserver/migrations/32.lua +++ b/data-otxserver/migrations/32.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 33 (wheel of destiny)") + logger.info("Updating database to version 32 (wheel of destiny)") db.query([[ CREATE TABLE IF NOT EXISTS `player_wheeldata` ( `player_id` int(11) NOT NULL, @@ -10,5 +10,4 @@ function onUpdateDatabase() ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - return true end diff --git a/data-otxserver/migrations/33.lua b/data-otxserver/migrations/33.lua index afac0cebd..7c0852a32 100644 --- a/data-otxserver/migrations/33.lua +++ b/data-otxserver/migrations/33.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 34 (add primary keys)") + logger.info("Updating database to version 33 (add primary keys)") db.query([[ ALTER TABLE `player_prey` ADD PRIMARY KEY (`player_id`, `slot`); @@ -24,5 +24,4 @@ function onUpdateDatabase() ALTER TABLE `player_wheeldata` ADD PRIMARY KEY (`player_id`); ]]) - return true end diff --git a/data-otxserver/migrations/34.lua b/data-otxserver/migrations/34.lua index 7f0a289f6..c344eae6b 100644 --- a/data-otxserver/migrations/34.lua +++ b/data-otxserver/migrations/34.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 35 (bosstiary tracker)") + logger.info("Updating database to version 34 (bosstiary tracker)") db.query("ALTER TABLE `player_bosstiary` ADD `tracker` blob NOT NULL;") - return true end diff --git a/data-otxserver/migrations/35.lua b/data-otxserver/migrations/35.lua index 10bddc471..9e2ab4dd1 100644 --- a/data-otxserver/migrations/35.lua +++ b/data-otxserver/migrations/35.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 36 (fix account premdays and lastday)") + logger.info("Updating database to version 35 (fix account premdays and lastday)") local resultQuery = db.storeQuery("SELECT `id`, `premdays`, `lastday` FROM `accounts` WHERE (`premdays` > 0 OR `lastday` > 0) AND `lastday` <= " .. os.time()) if resultQuery ~= false then @@ -12,7 +12,6 @@ function onUpdateDatabase() until not Result.next(resultQuery) Result.free(resultQuery) end - return true end function getNewValue(premDays, lastDay) diff --git a/data-otxserver/migrations/36.lua b/data-otxserver/migrations/36.lua index 7d35e223c..5fab551e4 100644 --- a/data-otxserver/migrations/36.lua +++ b/data-otxserver/migrations/36.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 37 (add coin_type to accounts)") + logger.info("Updating database to version 36 (add coin_type to accounts)") db.query("ALTER TABLE `coins_transactions` ADD `coin_type` tinyint(1) UNSIGNED NOT NULL DEFAULT '1';") - return true end diff --git a/data-otxserver/migrations/37.lua b/data-otxserver/migrations/37.lua index 70f2ec126..7219dfbba 100644 --- a/data-otxserver/migrations/37.lua +++ b/data-otxserver/migrations/37.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 38 (add pronoun to players)") + logger.info("Updating database to version 37 (add pronoun to players)") db.query("ALTER TABLE `players` ADD `pronoun` int(11) NOT NULL DEFAULT '0';") - return true end diff --git a/data-otxserver/migrations/38.lua b/data-otxserver/migrations/38.lua index 7981d5d70..7e9e37481 100644 --- a/data-otxserver/migrations/38.lua +++ b/data-otxserver/migrations/38.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 39 (create kv store)") + logger.info("Updating database to version 38 (create kv store)") db.query([[ CREATE TABLE IF NOT EXISTS `kv_store` ( `key_name` varchar(191) NOT NULL, @@ -8,5 +8,4 @@ function onUpdateDatabase() PRIMARY KEY (`key_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ]]) - return true end diff --git a/data-otxserver/migrations/39.lua b/data-otxserver/migrations/39.lua index 2bf381501..f660e98eb 100644 --- a/data-otxserver/migrations/39.lua +++ b/data-otxserver/migrations/39.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 40 (house transfer ownership on startup)") + logger.info("Updating database to version 39 (house transfer ownership on startup)") db.query("ALTER TABLE `houses` ADD `new_owner` int(11) NOT NULL DEFAULT '-1';") - return true end diff --git a/data-otxserver/migrations/4.lua b/data-otxserver/migrations/4.lua index fa07383ae..a7b044533 100644 --- a/data-otxserver/migrations/4.lua +++ b/data-otxserver/migrations/4.lua @@ -1,10 +1,9 @@ function onUpdateDatabase() - logger.info("Updating database to version 5 (boosted creature)") + logger.info("Updating database to version 4 (boosted creature)") db.query([[CREATE TABLE IF NOT EXISTS `boosted_creature` ( `boostname` TEXT, `date` varchar(250) NOT NULL DEFAULT '', `raceid` varchar(250) NOT NULL DEFAULT '', PRIMARY KEY (`date`) ) AS SELECT 0 AS date, "default" AS boostname, 0 AS raceid]]) - return true end diff --git a/data-otxserver/migrations/40.lua b/data-otxserver/migrations/40.lua index 22bfea0da..a7d3ae6af 100644 --- a/data-otxserver/migrations/40.lua +++ b/data-otxserver/migrations/40.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 41 (optimize house_lists)") + logger.info("Updating database to version 40 (optimize house_lists)") db.query([[ ALTER TABLE `house_lists` @@ -12,6 +12,4 @@ function onUpdateDatabase() ALTER TABLE `house_lists` MODIFY `version` bigint(20) NOT NULL DEFAULT '0'; ]]) - - return true end diff --git a/data-otxserver/migrations/41.lua b/data-otxserver/migrations/41.lua index 15eb1d88e..504bd9500 100644 --- a/data-otxserver/migrations/41.lua +++ b/data-otxserver/migrations/41.lua @@ -1,11 +1,9 @@ function onUpdateDatabase() - logger.info("Updating database to version 42 (fix xpboost types)") + logger.info("Updating database to version 41 (fix xpboost types)") db.query([[ ALTER TABLE `players` MODIFY `xpboost_stamina` smallint(5) UNSIGNED DEFAULT NULL, MODIFY `xpboost_value` tinyint(4) UNSIGNED DEFAULT NULL ]]) - - return true end diff --git a/data-otxserver/migrations/42.lua b/data-otxserver/migrations/42.lua index 4d07b663d..6bc750efa 100644 --- a/data-otxserver/migrations/42.lua +++ b/data-otxserver/migrations/42.lua @@ -1,10 +1,8 @@ function onUpdateDatabase() - logger.info("Updating database to version 43 (fix guildwar_kills_unique)") + logger.info("Updating database to version 42 (fix guildwar_kills_unique)") db.query([[ ALTER TABLE `guildwar_kills` DROP INDEX `guildwar_kills_unique` ]]) - - return true end diff --git a/data-otxserver/migrations/43.lua b/data-otxserver/migrations/43.lua index 6d3492a81..bcf165886 100644 --- a/data-otxserver/migrations/43.lua +++ b/data-otxserver/migrations/43.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 44 (feat frags_limit, payment and duration_days in guild wars)") + logger.info("Updating database to version 43 (feat frags_limit, payment and duration_days in guild wars)") db.query([[ ALTER TABLE `guild_wars` @@ -7,6 +7,4 @@ function onUpdateDatabase() ADD `payment` bigint(13) UNSIGNED NOT NULL DEFAULT '0', ADD `duration_days` tinyint(3) UNSIGNED NOT NULL DEFAULT '0' ]]) - - return true end diff --git a/data-otxserver/migrations/44.lua b/data-otxserver/migrations/44.lua index c551fc79a..acef11cee 100644 --- a/data-otxserver/migrations/44.lua +++ b/data-otxserver/migrations/44.lua @@ -1,11 +1,9 @@ function onUpdateDatabase() - logger.info("Updating database to version 45 (fix: mana shield column size for more than 65k)") + logger.info("Updating database to version 44 (fix: mana shield column size for more than 65k)") db.query([[ ALTER TABLE `players` MODIFY COLUMN `manashield` INT UNSIGNED NOT NULL DEFAULT '0', MODIFY COLUMN `max_manashield` INT UNSIGNED NOT NULL DEFAULT '0'; ]]) - - return true end diff --git a/data-otxserver/migrations/45.lua b/data-otxserver/migrations/45.lua index 4ceb5f7e3..abed34640 100644 --- a/data-otxserver/migrations/45.lua +++ b/data-otxserver/migrations/45.lua @@ -1,5 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 46 (feat: vip groups)") + logger.info("Updating database to version 45 (feat: vip groups)") db.query([[ CREATE TABLE IF NOT EXISTS `account_vipgroups` ( @@ -51,6 +51,4 @@ function onUpdateDatabase() INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`) SELECT 3, id, 'Trading Partners', 0 FROM `accounts`; ]]) - - return true end diff --git a/data-otxserver/migrations/46.lua b/data-otxserver/migrations/46.lua index 506da3a13..d7f24765b 100644 --- a/data-otxserver/migrations/46.lua +++ b/data-otxserver/migrations/46.lua @@ -1,7 +1,5 @@ function onUpdateDatabase() - logger.info("Updating database to version 47 (fix: creature speed and conditions)") + logger.info("Updating database to version 46 (fix: creature speed and conditions)") db.query("ALTER TABLE `players` MODIFY `conditions` mediumblob NOT NULL;") - - return true end diff --git a/data-otxserver/migrations/47.lua b/data-otxserver/migrations/47.lua index 86a6d8ffe..6b658e408 100644 --- a/data-otxserver/migrations/47.lua +++ b/data-otxserver/migrations/47.lua @@ -1,3 +1,24 @@ function onUpdateDatabase() - return false -- true = There are others migrations file | false = this is the last migration file + logger.info("Updating database to version 47 (hireling)") + + db.query([[ + CREATE TABLE IF NOT EXISTS `player_hirelings` ( + `id` INT NOT NULL PRIMARY KEY auto_increment, + `player_id` INT NOT NULL, + `name` varchar(255), + `active` tinyint unsigned NOT NULL DEFAULT '0', + `sex` tinyint unsigned NOT NULL DEFAULT '0', + `posx` int(11) NOT NULL DEFAULT '0', + `posy` int(11) NOT NULL DEFAULT '0', + `posz` int(11) NOT NULL DEFAULT '0', + `lookbody` int(11) NOT NULL DEFAULT '0', + `lookfeet` int(11) NOT NULL DEFAULT '0', + `lookhead` int(11) NOT NULL DEFAULT '0', + `looklegs` int(11) NOT NULL DEFAULT '0', + `looktype` int(11) NOT NULL DEFAULT '136', + + FOREIGN KEY(`player_id`) REFERENCES `players`(`id`) + ON DELETE CASCADE + ) + ]]) end diff --git a/data-otxserver/migrations/48.lua b/data-otxserver/migrations/48.lua new file mode 100644 index 000000000..53d6ba3a9 --- /dev/null +++ b/data-otxserver/migrations/48.lua @@ -0,0 +1,27 @@ +function onUpdateDatabase() + logger.info("Updating database to version 48 (House Auction)") + + db.query([[ + ALTER TABLE `houses` + DROP `bid`, + DROP `bid_end`, + DROP `last_bid`, + DROP `highest_bidder` + ]]) + + db.query([[ + ALTER TABLE `houses` + ADD `bidder` int(11) NOT NULL DEFAULT '0', + ADD `bidder_name` varchar(255) NOT NULL DEFAULT '', + ADD `highest_bid` int(11) NOT NULL DEFAULT '0', + ADD `internal_bid` int(11) NOT NULL DEFAULT '0', + ADD `bid_end_date` int(11) NOT NULL DEFAULT '0', + ADD `state` smallint(5) UNSIGNED NOT NULL DEFAULT '0', + ADD `transfer_status` tinyint(1) DEFAULT '0' + ]]) + + db.query([[ + ALTER TABLE `accounts` + ADD `house_bid_id` int(11) NOT NULL DEFAULT '0' + ]]) +end diff --git a/data-otxserver/migrations/5.lua b/data-otxserver/migrations/5.lua index 50a02a7fe..4b027c200 100644 --- a/data-otxserver/migrations/5.lua +++ b/data-otxserver/migrations/5.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 6 (quickloot)") + logger.info("Updating database to version 5 (quickloot)") db.query("ALTER TABLE `players` ADD `quickloot_fallback` TINYINT DEFAULT 0") - return true end diff --git a/data-otxserver/migrations/6.lua b/data-otxserver/migrations/6.lua index 905a02ac1..cc3a3f764 100644 --- a/data-otxserver/migrations/6.lua +++ b/data-otxserver/migrations/6.lua @@ -1,8 +1,7 @@ function onUpdateDatabase() - logger.info("Updating database to version 7 (Stash supply)") + logger.info("Updating database to version 6 (Stash supply)") db.query([[CREATE TABLE IF NOT EXISTS `player_stash` ( `player_id` INT(16) NOT NULL, `item_id` INT(16) NOT NULL, `item_count` INT(32) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;]]) - return true -- true = There are others migrations file | false = this is the last migration file end diff --git a/data-otxserver/migrations/7.lua b/data-otxserver/migrations/7.lua index f5c7e6ed5..0666d7cf9 100644 --- a/data-otxserver/migrations/7.lua +++ b/data-otxserver/migrations/7.lua @@ -1,5 +1,4 @@ function onUpdateDatabase() - logger.info("Updating database to version 8 (recruiter system)") + logger.info("Updating database to version 7 (recruiter system)") db.query("ALTER TABLE `accounts` ADD `recruiter` INT(6) DEFAULT 0") - return true end diff --git a/data-otxserver/migrations/8.lua b/data-otxserver/migrations/8.lua index bab5e9087..a96db82e2 100644 --- a/data-otxserver/migrations/8.lua +++ b/data-otxserver/migrations/8.lua @@ -1,30 +1,30 @@ function onUpdateDatabase() - logger.info("Updating database to version 9 (Bestiary cpp)") + logger.info("Updating database to version 8 (Bestiary cpp)") db.query([[CREATE TABLE IF NOT EXISTS `player_charms` ( -`player_guid` INT(250) NOT NULL , -`charm_points` VARCHAR(250) NULL , -`charm_expansion` BOOLEAN NULL , -`rune_wound` INT(250) NULL , -`rune_enflame` INT(250) NULL , -`rune_poison` INT(250) NULL , -`rune_freeze` INT(250) NULL , -`rune_zap` INT(250) NULL , -`rune_curse` INT(250) NULL , -`rune_cripple` INT(250) NULL , -`rune_parry` INT(250) NULL , -`rune_dodge` INT(250) NULL , -`rune_adrenaline` INT(250) NULL , -`rune_numb` INT(250) NULL, -`rune_cleanse` INT(250) NULL , -`rune_bless` INT(250) NULL , -`rune_scavenge` INT(250) NULL , -`rune_gut` INT(250) NULL , -`rune_low_blow` INT(250) NULL , -`rune_divine` INT(250) NULL , -`rune_vamp` INT(250) NULL , -`rune_void` INT(250) NULL , -`UsedRunesBit` VARCHAR(250) NULL , -`UnlockedRunesBit` VARCHAR(250) NULL, -`tracker list` BLOB NULL ) ENGINE = InnoDB DEFAULT CHARSET=utf8;]]) - return true -- true = There are others migrations file | false = this is the last migration file + `player_guid` INT(250) NOT NULL , + `charm_points` VARCHAR(250) NULL , + `charm_expansion` BOOLEAN NULL , + `rune_wound` INT(250) NULL , + `rune_enflame` INT(250) NULL , + `rune_poison` INT(250) NULL , + `rune_freeze` INT(250) NULL , + `rune_zap` INT(250) NULL , + `rune_curse` INT(250) NULL , + `rune_cripple` INT(250) NULL , + `rune_parry` INT(250) NULL , + `rune_dodge` INT(250) NULL , + `rune_adrenaline` INT(250) NULL , + `rune_numb` INT(250) NULL, + `rune_cleanse` INT(250) NULL , + `rune_bless` INT(250) NULL , + `rune_scavenge` INT(250) NULL , + `rune_gut` INT(250) NULL , + `rune_low_blow` INT(250) NULL , + `rune_divine` INT(250) NULL , + `rune_vamp` INT(250) NULL , + `rune_void` INT(250) NULL , + `UsedRunesBit` VARCHAR(250) NULL , + `UnlockedRunesBit` VARCHAR(250) NULL, + `tracker list` BLOB NULL ) ENGINE = InnoDB DEFAULT CHARSET=utf8; + ]]) end diff --git a/data-otxserver/migrations/9.lua b/data-otxserver/migrations/9.lua index 23aa28daf..23516833f 100644 --- a/data-otxserver/migrations/9.lua +++ b/data-otxserver/migrations/9.lua @@ -1,9 +1,8 @@ function onUpdateDatabase() - logger.info("Updating database to version 10 (Mount Colors and familiars)") + logger.info("Updating database to version 9 (Mount Colors and familiars)") db.query("ALTER TABLE `players` ADD `lookmountbody` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookmountfeet` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookmounthead` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookmountlegs` tinyint(3) unsigned NOT NULL DEFAULT '0'") db.query("ALTER TABLE `players` ADD `lookfamiliarstype` int(11) unsigned NOT NULL DEFAULT '0'") - return true end diff --git a/data-otxserver/migrations/README.md b/data-otxserver/migrations/README.md new file mode 100644 index 000000000..b23473bf2 --- /dev/null +++ b/data-otxserver/migrations/README.md @@ -0,0 +1,45 @@ +# Database Migration System + +This document provides an overview of the current database migration system for the project. The migration process has been streamlined to ensure that all migration scripts are automatically applied in order, making it easier to maintain database updates. + +## How It Works + +The migration system is designed to apply updates to the database schema or data whenever a new server version is started. Migration scripts are stored in the `migrations` directory, and the system will automatically apply any scripts that have not yet been executed. + +### Steps Involved + +1. **Retrieve Current Database Version**: + - The system first retrieves the current version of the database using `getDatabaseVersion()`. + - This version is used to determine which migration scripts need to be executed. + +2. **Migration Files Directory**: + - All migration scripts are stored in the `migrations` directory. + - Each migration script is named using a numerical pattern, such as `1.lua`, `2.lua`, etc. + - The naming convention helps determine the order in which scripts should be applied. + +3. **Execute Migration Scripts**: + - The migration system iterates through the migration directory and applies each migration script that has a version greater than the current database version. + - Only scripts that have not been applied are executed. + - The Lua state (`lua_State* L`) is initialized to run each script. + +4. **Update Database Version**: + - After each migration script is successfully applied, the system updates the database version to reflect the applied change. + - This ensures that the script is not re-applied on subsequent server startups. + +## Example Migration Script + +Below is an example of what a migration script might look like. Note that no return value is required, as all migration files are applied based on the current database version. + +```lua +-- Migration script example (for documentation purposes only) +-- This migration script should include all necessary SQL commands or operations to apply a specific update to the database. + +-- Example: Adding a new column to the "players" table +local query = [[ + ALTER TABLE players ADD COLUMN new_feature_flag TINYINT(1) NOT NULL DEFAULT 0; +]] + +-- Execute the query +db.execute(query) -- This function executes the given SQL query on the database. + +-- Note: Ensure that queries are validated to avoid errors during the migration process. diff --git a/data-otxserver/monster/bosses/bakragore.lua b/data-otxserver/monster/bosses/bakragore.lua new file mode 100644 index 000000000..c13592e7a --- /dev/null +++ b/data-otxserver/monster/bosses/bakragore.lua @@ -0,0 +1,154 @@ +local mType = Game.createMonsterType("Bakragore") +local monster = {} + +monster.description = "Bakragore" +monster.experience = 15000000 +monster.outfit = { + lookType = 1671, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.events = { + "RottenBloodBakragoreDeath", +} + +monster.bosstiary = { + bossRaceId = 2367, + bossRace = RARITY_NEMESIS, +} + +monster.health = 660000 +monster.maxHealth = 660000 +monster.race = "undead" +monster.corpse = 44012 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 10000, + chance = 20, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = true, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.summon = { + maxSummons = 2, + summons = { + { name = "Elder Bloodjaw", chance = 20, interval = 2000, count = 2 }, + }, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "Light ... darkens!", yell = false }, + { text = "Light .. the ... darkness!", yell = false }, + { text = "Darkness ... is ... light!", yell = false }, + { text = "WILL ... PUNISH ... YOU!", yell = false }, + { text = "RAAAR!", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 8938, maxCount = 165 }, + { name = "ultimate mana potion", chance = 11433, maxCount = 198 }, + { name = "giant amethyst", chance = 10570, maxCount = 4 }, + { name = "giant topaz", chance = 10570, maxCount = 6 }, + { name = "ultimate spirit potion", chance = 11433, maxCount = 45 }, + { name = "giant ruby", chance = 10570, maxCount = 1 }, + { name = "giant sapphire", chance = 10570, maxCount = 1 }, + { name = "mastermind potion", chance = 10938, maxCount = 23 }, + { id = 3039, chance = 10570, maxCount = 3 }, -- red gem + { name = "violet gem", chance = 10970, maxCount = 8 }, + { name = "yellow gem", chance = 10970, maxCount = 9 }, + -- { name = "figurine of bakragore", chance = 10970 }, + -- { name = "bakragore's amalgamation", chance = 570 }, + { name = "spiritual horseshoe", chance = 470 }, + { id = 43895, chance = 360 }, -- Bag you covet +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -3000 }, + { name = "combat", interval = 3000, chance = 35, type = COMBAT_ICEDAMAGE, minDamage = -900, maxDamage = -1100, range = 7, radius = 7, shootEffect = CONST_ANI_ICE, effect = 243, target = true }, + { name = "combat", interval = 2000, chance = 13, type = COMBAT_DEATHDAMAGE, minDamage = -100, maxDamage = -1000, length = 8, spread = 0, effect = 252, target = false }, + { name = "combat", interval = 3000, chance = 30, type = COMBAT_FIREDAMAGE, minDamage = -1000, maxDamage = -2000, length = 8, spread = 0, effect = 249, target = false }, + { name = "combat", interval = 2000, chance = 30, type = COMBAT_ICEDAMAGE, minDamage = -950, maxDamage = -2400, range = 7, radius = 3, shootEffect = 37, effect = 240, target = true }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_DEATHDAMAGE, minDamage = -1000, maxDamage = -2500, length = 8, spread = 0, effect = 244, target = false }, +} + +monster.defenses = { + defense = 135, + armor = 135, + { name = "combat", interval = 3000, chance = 15, type = COMBAT_HEALING, minDamage = 2500, maxDamage = 3500, effect = 236, target = false }, + { name = "speed", interval = 4000, chance = 80, speedChange = 700, effect = CONST_ME_MAGIC_RED, target = false, duration = 6000 }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, + { type = COMBAT_ENERGYDAMAGE, percent = 15 }, + { type = COMBAT_EARTHDAMAGE, percent = 15 }, + { type = COMBAT_FIREDAMAGE, percent = 15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 15 }, + { type = COMBAT_HOLYDAMAGE, percent = 15 }, + { type = COMBAT_DEATHDAMAGE, percent = 15 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onThink = function(monster, interval) end + +mType.onAppear = function(monster, creature) + if monster:getType():isRewardBoss() then + monster:setReward(true) + end +end + +mType.onDisappear = function(monster, creature) end + +mType.onMove = function(monster, creature, fromPosition, toPosition) end + +mType.onSay = function(monster, creature, type, message) end + +mType:register(monster) diff --git a/data-otxserver/monster/bosses/chagorz.lua b/data-otxserver/monster/bosses/chagorz.lua new file mode 100644 index 000000000..bec5b74e9 --- /dev/null +++ b/data-otxserver/monster/bosses/chagorz.lua @@ -0,0 +1,147 @@ +local mType = Game.createMonsterType("Chagorz") +local monster = {} + +monster.description = "Chagorz" +monster.experience = 3250000 +monster.outfit = { + lookType = 1665, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.events = { + "RottenBloodBossDeath", +} + +monster.bosstiary = { + bossRaceId = 2366, + bossRace = RARITY_ARCHFOE, +} + +monster.health = 350000 +monster.maxHealth = 350000 +monster.race = "undead" +monster.corpse = 44024 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 10000, + chance = 20, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = true, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.summon = {} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "The light... that... drains!", yell = false }, + { text = "RAAAR!", yell = false }, + { text = "WILL ... PUNISH ... YOU!", yell = false }, + { text = "Darkness ... devours!", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 5441, maxCount = 108 }, + { name = "mastermind potion", chance = 5530, maxCount = 28 }, + { name = "supreme health potion", chance = 5044, maxCount = 154 }, + { name = "giant sapphire", chance = 10546, maxCount = 1 }, + { name = "ultimate mana potion", chance = 5752, maxCount = 107 }, + { name = "violet gem", chance = 13217, maxCount = 4 }, + { id = 3039, chance = 13465, maxCount = 1 }, -- red gem + { name = "yellow gem", chance = 14071, maxCount = 1 }, + { name = "blue gem", chance = 11156, maxCount = 3 }, + { name = "bullseye potion", chance = 6792, maxCount = 21 }, + { name = "giant amethyst", chance = 11603, maxCount = 1 }, + { name = "giant topaz", chance = 12280, maxCount = 1 }, + { name = "green gem", chance = 8348, maxCount = 1 }, + { name = "ultimate spirit potion", chance = 10934, maxCount = 18 }, + { name = "white gem", chance = 9600, maxCount = 3 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -1300, maxDamage = -2250 }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_EARTHDAMAGE, minDamage = -500, maxDamage = -900, radius = 4, effect = CONST_ME_GREEN_RINGS, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_DEATHDAMAGE, minDamage = -500, maxDamage = -900, range = 4, radius = 4, effect = 241, target = true }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_EARTHDAMAGE, minDamage = -1000, maxDamage = -1200, length = 10, spread = 0, effect = CONST_ME_POFF, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_LIFEDRAIN, minDamage = -1500, maxDamage = -1900, length = 10, spread = 0, effect = 225, target = false }, + { name = "speed", interval = 2000, chance = 20, speedChange = -600, radius = 7, effect = CONST_ME_MAGIC_GREEN, target = false, duration = 20000 }, +} + +monster.defenses = { + defense = 105, + armor = 105, + { name = "combat", interval = 3000, chance = 10, type = COMBAT_HEALING, minDamage = 700, maxDamage = 1500, effect = 236, target = false }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, + { type = COMBAT_ENERGYDAMAGE, percent = 15 }, + { type = COMBAT_EARTHDAMAGE, percent = 15 }, + { type = COMBAT_FIREDAMAGE, percent = 15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 15 }, + { type = COMBAT_HOLYDAMAGE, percent = 15 }, + { type = COMBAT_DEATHDAMAGE, percent = 15 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onThink = function(monster, interval) end + +mType.onAppear = function(monster, creature) + if monster:getType():isRewardBoss() then + monster:setReward(true) + end +end + +mType.onDisappear = function(monster, creature) end + +mType.onMove = function(monster, creature, fromPosition, toPosition) end + +mType.onSay = function(monster, creature, type, message) end + +mType:register(monster) diff --git a/data-otxserver/monster/bosses/ichgahal.lua b/data-otxserver/monster/bosses/ichgahal.lua new file mode 100644 index 000000000..7d0d21924 --- /dev/null +++ b/data-otxserver/monster/bosses/ichgahal.lua @@ -0,0 +1,154 @@ +local mType = Game.createMonsterType("Ichgahal") +local monster = {} + +monster.description = "Ichgahal" +monster.experience = 3250000 +monster.outfit = { + lookType = 1665, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.events = { + "RottenBloodBossDeath", +} + +monster.bosstiary = { + bossRaceId = 2364, + bossRace = RARITY_NEMESIS, +} + +monster.health = 350000 +monster.maxHealth = 350000 +monster.race = "undead" +monster.corpse = 44018 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 10000, + chance = 20, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = true, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.summon = { + maxSummons = 8, + summons = { + { name = "Mushroom", chance = 30, interval = 5000, count = 8 }, + }, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "Rott!!", yell = false }, + { text = "Putrefy!", yell = false }, + { text = "Decay!", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 14615, maxCount = 115 }, + { name = "ultimate spirit potion", chance = 7169, maxCount = 153 }, + { name = "mastermind potion", chance = 14651, maxCount = 45 }, + { name = "yellow gem", chance = 9243, maxCount = 5 }, + { name = "amber with a bug", chance = 7224, maxCount = 2 }, + { name = "ultimate mana potion", chance = 13137, maxCount = 179 }, + { name = "violet gem", chance = 14447, maxCount = 4 }, + { name = "raw watermelon tourmaline", chance = 6788, maxCount = 2 }, + { id = 3039, chance = 9047, maxCount = 1 }, -- red gem + { name = "supreme health potion", chance = 14635, maxCount = 37 }, + { name = "berserk potion", chance = 14973, maxCount = 45 }, + { name = "amber with a dragonfly", chance = 6470, maxCount = 1 }, + { name = "gold ingot", chance = 11421, maxCount = 1 }, + { name = "blue gem", chance = 8394, maxCount = 1 }, + { name = "bullseye potion", chance = 13783, maxCount = 36 }, + { name = "putrefactive figurine", chance = 11416, maxCount = 1 }, + { name = "ichgahal's fungal infestation", chance = 7902, maxCount = 1 }, + { name = "white gem", chance = 13559, maxCount = 3 }, + { id = 43895, chance = 360 }, -- Bag you covet +} + +monster.attacks = { + { name = "melee", interval = 3000, chance = 100, minDamage = -1500, maxDamage = -2300 }, + { name = "combat", interval = 1000, chance = 10, type = COMBAT_PHYSICALDAMAGE, minDamage = -700, maxDamage = -1000, length = 12, spread = 0, effect = 249, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_MANADRAIN, minDamage = -2600, maxDamage = -2300, length = 12, spread = 0, effect = 193, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -900, maxDamage = -1500, length = 6, spread = 0, effect = CONST_ME_FIREAREA, target = false }, + { name = "speed", interval = 2000, chance = 35, speedChange = -600, radius = 8, effect = CONST_ME_MAGIC_RED, target = false, duration = 15000 }, +} + +monster.defenses = { + defense = 105, + armor = 105, + { name = "combat", interval = 3000, chance = 10, type = COMBAT_HEALING, minDamage = 800, maxDamage = 1200, effect = 236, target = false }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, + { type = COMBAT_ENERGYDAMAGE, percent = 15 }, + { type = COMBAT_EARTHDAMAGE, percent = 15 }, + { type = COMBAT_FIREDAMAGE, percent = 15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 15 }, + { type = COMBAT_HOLYDAMAGE, percent = 15 }, + { type = COMBAT_DEATHDAMAGE, percent = 15 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onThink = function(monster, interval) end + +mType.onAppear = function(monster, creature) + if monster:getType():isRewardBoss() then + monster:setReward(true) + end +end + +mType.onDisappear = function(monster, creature) end + +mType.onMove = function(monster, creature, fromPosition, toPosition) end + +mType.onSay = function(monster, creature, type, message) end + +mType:register(monster) diff --git a/data-otxserver/monster/bosses/murcion.lua b/data-otxserver/monster/bosses/murcion.lua new file mode 100644 index 000000000..ad091f747 --- /dev/null +++ b/data-otxserver/monster/bosses/murcion.lua @@ -0,0 +1,140 @@ +local mType = Game.createMonsterType("Murcion") +local monster = {} + +monster.description = "Murcion" +monster.experience = 3250000 +monster.outfit = { + lookType = 1664, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.events = { + "RottenBloodBossDeath", +} + +monster.bosstiary = { + bossRaceId = 2362, + bossRace = RARITY_NEMESIS, +} + +monster.health = 350000 +monster.maxHealth = 350000 +monster.race = "undead" +monster.corpse = 44015 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 10000, + chance = 20, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = true, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.summon = { + maxSummons = 8, + summons = { + { name = "Mushroom", chance = 30, interval = 5000, count = 8 }, + }, +} + +monster.voices = {} + +monster.loot = { + { name = "crystal coin", chance = 12317, maxCount = 91 }, + { id = 3039, chance = 10896, maxCount = 2 }, -- red gem + { name = "amber with a bug", chance = 14590, maxCount = 1 }, + { name = "amber with a dragonfly", chance = 5405, maxCount = 1 }, + { name = "bullseye potion", chance = 10821, maxCount = 44 }, + { name = "green gem", chance = 7763, maxCount = 4 }, + { name = "mastermind potion", chance = 9534, maxCount = 15 }, + { name = "supreme health potion", chance = 6212, maxCount = 102 }, + { name = "ultimate mana potion", chance = 8785, maxCount = 29 }, + { name = "ultimate spirit potion", chance = 8783, maxCount = 161 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -1400, maxDamage = -2300 }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_DEATHDAMAGE, minDamage = -500, maxDamage = -900, radius = 4, effect = CONST_ME_SMALLCLOUDS, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_HOLYDAMAGE, minDamage = -500, maxDamage = -900, range = 4, radius = 4, shootEffect = 31, effect = 248, target = true }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_ICEDAMAGE, minDamage = -1000, maxDamage = -1200, length = 10, spread = 0, effect = 53, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -1500, maxDamage = -1900, length = 10, spread = 0, effect = 158, target = false }, + { name = "speed", interval = 2000, chance = 20, speedChange = -600, radius = 7, effect = CONST_ME_POFF, target = false, duration = 20000 }, +} + +monster.defenses = { + defense = 105, + armor = 105, + { name = "combat", interval = 3000, chance = 10, type = COMBAT_HEALING, minDamage = 800, maxDamage = 1500, effect = 236, target = false }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, + { type = COMBAT_ENERGYDAMAGE, percent = 15 }, + { type = COMBAT_EARTHDAMAGE, percent = 15 }, + { type = COMBAT_FIREDAMAGE, percent = 15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 15 }, + { type = COMBAT_HOLYDAMAGE, percent = 15 }, + { type = COMBAT_DEATHDAMAGE, percent = 15 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onThink = function(monster, interval) end + +mType.onAppear = function(monster, creature) + if monster:getType():isRewardBoss() then + monster:setReward(true) + end +end + +mType.onDisappear = function(monster, creature) end + +mType.onMove = function(monster, creature, fromPosition, toPosition) end + +mType.onSay = function(monster, creature, type, message) end + +mType:register(monster) diff --git a/data-otxserver/monster/bosses/vemiath.lua b/data-otxserver/monster/bosses/vemiath.lua new file mode 100644 index 000000000..97be142a6 --- /dev/null +++ b/data-otxserver/monster/bosses/vemiath.lua @@ -0,0 +1,151 @@ +local mType = Game.createMonsterType("Vemiath") +local monster = {} + +monster.description = "Vemiath" +monster.experience = 3250000 +monster.outfit = { + lookType = 1665, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.events = { + "RottenBloodBossDeath", +} + +monster.bosstiary = { + bossRaceId = 2365, + bossRace = RARITY_ARCHFOE, +} + +monster.health = 350000 +monster.maxHealth = 350000 +monster.race = "undead" +monster.corpse = 44021 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 10000, + chance = 20, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = true, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.summon = {} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "The light... that... drains!", yell = false }, + { text = "RAAAR!", yell = false }, + { text = "WILL ... PUNISH ... YOU!", yell = false }, + { text = "Darkness ... devours!", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 8852, maxCount = 125 }, + { name = "ultimate mana potion", chance = 11337, maxCount = 211 }, + { name = "giant emerald", chance = 6423, maxCount = 1 }, + { name = "supreme health potion", chance = 8385, maxCount = 179 }, + { name = "yellow gem", chance = 8604, maxCount = 5 }, + { name = "berserk potion", chance = 9395, maxCount = 45 }, + { name = "blue gem", chance = 14144, maxCount = 5 }, + { name = "green gem", chance = 6221, maxCount = 4 }, + { name = "bullseye potion", chance = 6530, maxCount = 26 }, + { name = "mastermind potion", chance = 5700, maxCount = 44 }, + { name = "ultimate spirit potion", chance = 9216, maxCount = 25 }, + { name = "giant topaz", chance = 11191, maxCount = 1 }, + { name = "giant amethyst", chance = 8527, maxCount = 1 }, + { name = "gold ingot", chance = 10866, maxCount = 1 }, + { id = 3039, chance = 8945, maxCount = 1 }, -- red gem + { name = "dragon figurine", chance = 11502, maxCount = 1 }, + { name = "raw watermelon tourmaline", chance = 9302, maxCount = 1 }, + { name = "vemiath's infused basalt", chance = 7914, maxCount = 1 }, + { name = "violet gem", chance = 7210, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -1500, maxDamage = -2500 }, + { name = "combat", interval = 3000, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -500, maxDamage = -1000, length = 10, spread = 3, effect = 244, target = false }, + { name = "speed", interval = 2000, chance = 25, speedChange = -600, radius = 7, effect = CONST_ME_MAGIC_RED, target = false, duration = 15000 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_ICEDAMAGE, minDamage = -300, maxDamage = -700, radius = 5, effect = 243, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_DEATHDAMAGE, minDamage = -500, maxDamage = -800, length = 10, spread = 3, effect = CONST_ME_EXPLOSIONHIT, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -500, maxDamage = -800, length = 8, spread = 3, effect = CONST_ME_FIREATTACK, target = false }, +} + +monster.defenses = { + defense = 105, + armor = 105, + { name = "combat", interval = 3000, chance = 10, type = COMBAT_HEALING, minDamage = 800, maxDamage = 1500, effect = 236, target = false }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 15 }, + { type = COMBAT_ENERGYDAMAGE, percent = 15 }, + { type = COMBAT_EARTHDAMAGE, percent = 15 }, + { type = COMBAT_FIREDAMAGE, percent = 15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 15 }, + { type = COMBAT_HOLYDAMAGE, percent = 15 }, + { type = COMBAT_DEATHDAMAGE, percent = 15 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType.onThink = function(monster, interval) end + +mType.onAppear = function(monster, creature) + if monster:getType():isRewardBoss() then + monster:setReward(true) + end +end + +mType.onDisappear = function(monster, creature) end + +mType.onMove = function(monster, creature, fromPosition, toPosition) end + +mType.onSay = function(monster, creature, type, message) end + +mType:register(monster) diff --git a/data-otxserver/monster/elementals/lava_lurker.lua b/data-otxserver/monster/elementals/lava_lurker.lua index 6a6e236f8..f7a88445c 100644 --- a/data-otxserver/monster/elementals/lava_lurker.lua +++ b/data-otxserver/monster/elementals/lava_lurker.lua @@ -95,7 +95,7 @@ monster.elements = { { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, { type = COMBAT_ENERGYDAMAGE, percent = 0 }, { type = COMBAT_EARTHDAMAGE, percent = 0 }, - { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, { type = COMBAT_LIFEDRAIN, percent = 0 }, { type = COMBAT_MANADRAIN, percent = 0 }, { type = COMBAT_DROWNDAMAGE, percent = 0 }, @@ -111,4 +111,8 @@ monster.immunities = { { type = "bleed", condition = false }, } +monster.heals = { + { type = COMBAT_FIREDAMAGE, percent = 100 }, +} + mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/bloated_man-maggot.lua b/data-otxserver/monster/quests/rotten_blood/bloated_man-maggot.lua new file mode 100644 index 000000000..76c2ecb9f --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/bloated_man-maggot.lua @@ -0,0 +1,122 @@ +local mType = Game.createMonsterType("Bloated Man-Maggot") +local monster = {} + +monster.description = "a bloated man-maggot" +monster.experience = 21570 +monster.outfit = { + lookType = 1654, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2392 +monster.Bestiary = { + class = "Vermin", + race = BESTY_RACE_VERMIN, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Jaded Roots", +} + +monster.health = 31700 +monster.maxHealth = 31700 +monster.race = "undead" +monster.corpse = 43816 +monster.speed = 195 +monster.manaCost = 305 + +monster.changeTarget = { + interval = 5000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = true, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = {} + +monster.loot = { + { name = "crystal coin", chance = 12961, maxCount = 1 }, + { name = "organic acid", chance = 11678, maxCount = 1 }, + { name = "might ring", chance = 10020, maxCount = 1 }, + { name = "small emerald", chance = 9133, maxCount = 5 }, + { name = "rotten roots", chance = 8637, maxCount = 1 }, + { name = "bloated maggot", chance = 8133, maxCount = 1 }, + { name = "terra rod", chance = 8078, maxCount = 1 }, + { name = "butcher's axe", chance = 7967, maxCount = 1 }, + { name = "blue gem", chance = 7808, maxCount = 1 }, + { name = "violet gem", chance = 7084, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1500 }, + { name = "combat", interval = 2500, chance = 25, type = COMBAT_PHYSICALDAMAGE, minDamage = -1400, maxDamage = -1700, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = true }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_EARTHDAMAGE, minDamage = -1400, maxDamage = -1900, radius = 5, effect = CONST_ME_BIGPLANTS, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -1400, maxDamage = -1550, length = 8, spread = 3, effect = CONST_ME_GROUNDSHAKER, target = false }, + { name = "largefirering", interval = 2500, chance = 15, minDamage = -1400, maxDamage = -1800, target = false }, +} + +monster.defenses = { + defense = 104, + armor = 104, + mitigation = 3.16, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 45 }, + { type = COMBAT_ENERGYDAMAGE, percent = -15 }, + { type = COMBAT_EARTHDAMAGE, percent = 40 }, + { type = COMBAT_FIREDAMAGE, percent = 15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = -15 }, + { type = COMBAT_HOLYDAMAGE, percent = -5 }, + { type = COMBAT_DEATHDAMAGE, percent = 5 }, +} + +monster.immunities = { + { type = "paralyze", condition = false }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/converter.lua b/data-otxserver/monster/quests/rotten_blood/converter.lua new file mode 100644 index 000000000..65ebab919 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/converter.lua @@ -0,0 +1,119 @@ +local mType = Game.createMonsterType("Converter") +local monster = {} + +monster.description = "a converter" +monster.experience = 21425 +monster.outfit = { + lookType = 1623, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2379 +monster.Bestiary = { + class = "Elemental", + race = BESTY_RACE_ELEMENTAL, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Gloom Pillars.", +} + +monster.health = 29600 +monster.maxHealth = 29600 +monster.race = "undead" +monster.corpse = 43567 +monster.speed = 250 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = true, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 80, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = false, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.loot = { + { name = "crystal coin", chance = 5230, maxCount = 1 }, + { name = "darklight obsidian axe", chance = 6963, maxCount = 1 }, + { name = "darklight matter", chance = 6927, maxCount = 1 }, + { name = "darklight core", chance = 10715, maxCount = 1 }, + { name = "wand of starstorm", chance = 8797, maxCount = 1 }, + { name = "blue gem", chance = 9372, maxCount = 1 }, + { name = "ultimate health potion", chance = 9851, maxCount = 5 }, + { name = "focus cape", chance = 6945, maxCount = 1 }, + { name = "white gem", chance = 14533, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -900 }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_ENERGYDAMAGE, minDamage = -1400, maxDamage = -1900, length = 7, spread = 0, effect = CONST_ME_PINK_ENERGY_SPARK, target = false }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_HOLYDAMAGE, minDamage = -1500, maxDamage = -1600, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = true }, + { name = "largeholyring", interval = 2000, chance = 15, minDamage = -1400, maxDamage = -1900 }, + { name = "energy chain", interval = 3200, chance = 20, minDamage = -800, maxDamage = -1200 }, +} + +monster.defenses = { + defense = 100, + armor = 100, + mitigation = 3.31, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -20 }, + { type = COMBAT_ENERGYDAMAGE, percent = -10 }, + { type = COMBAT_EARTHDAMAGE, percent = 10 }, + { type = COMBAT_FIREDAMAGE, percent = 25 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 35 }, + { type = COMBAT_DEATHDAMAGE, percent = -15 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/darklight_construct.lua b/data-otxserver/monster/quests/rotten_blood/darklight_construct.lua new file mode 100644 index 000000000..c4b2c5e85 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/darklight_construct.lua @@ -0,0 +1,120 @@ +local mType = Game.createMonsterType("Darklight Construct") +local monster = {} + +monster.description = "a darklight construct" +monster.experience = 22050 +monster.outfit = { + lookType = 1622, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2378 +monster.Bestiary = { + class = "Magical", + race = BESTY_RACE_MAGICAL, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Darklight Core", +} + +monster.health = 32200 +monster.maxHealth = 32200 +monster.race = "undead" +monster.corpse = 43840 +monster.speed = 220 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.loot = { + { name = "crystal coin", chance = 11290, maxCount = 1 }, + { name = "dark obsidian splinter", chance = 12735, maxCount = 1 }, + { id = 3039, chance = 8781, maxCount = 1 }, -- red gem + { name = "small emerald", chance = 6646, maxCount = 3 }, + { name = "zaoan shoes", chance = 8614, maxCount = 1 }, + { name = "darklight core", chance = 5659, maxCount = 1 }, + { name = "darklight obsidian axe", chance = 11129, maxCount = 1 }, + { name = "magma amulet", chance = 13240, maxCount = 1 }, + { name = "small ruby", chance = 12458, maxCount = 3 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1050 }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -1300, maxDamage = -1500, length = 8, spread = 3, effect = CONST_ME_HITBYFIRE, target = false }, + { name = "combat", interval = 2000, chance = 25, type = COMBAT_FIREDAMAGE, minDamage = -1100, maxDamage = -1400, radius = 5, effect = CONST_ME_HITBYFIRE, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_HOLYDAMAGE, minDamage = -1500, maxDamage = -1600, radius = 5, effect = CONST_ME_HOLYAREA, target = true }, + { name = "extended fire chain", interval = 2000, chance = 15, minDamage = -800, maxDamage = -1200, target = true }, + { name = "largefirering", interval = 2800, chance = 20, minDamage = -1000, maxDamage = -1300, target = false }, +} + +monster.defenses = { + defense = 117, + armor = 117, + mitigation = 2.98, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -15 }, + { type = COMBAT_ENERGYDAMAGE, percent = -5 }, + { type = COMBAT_EARTHDAMAGE, percent = 10 }, + { type = COMBAT_FIREDAMAGE, percent = 55 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = -5 }, + { type = COMBAT_HOLYDAMAGE, percent = 40 }, + { type = COMBAT_DEATHDAMAGE, percent = -20 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/darklight_emitter.lua b/data-otxserver/monster/quests/rotten_blood/darklight_emitter.lua new file mode 100644 index 000000000..dc211745d --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/darklight_emitter.lua @@ -0,0 +1,118 @@ +local mType = Game.createMonsterType("Darklight Emitter") +local monster = {} + +monster.description = "a darklight emitter" +monster.experience = 20600 +monster.outfit = { + lookType = 1627, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2382 +monster.Bestiary = { + class = "Magical", + race = BESTY_RACE_MAGICAL, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Darklight Core", +} + +monster.health = 27500 +monster.maxHealth = 27500 +monster.race = "undead" +monster.corpse = 43583 +monster.speed = 210 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.loot = { + { name = "crystal coin", chance = 12516, maxCount = 2 }, + { name = "darklight core", chance = 13367, maxCount = 1 }, + { name = "darklight obsidian axe", chance = 10433, maxCount = 1 }, + { name = "zaoan armor", chance = 8574, maxCount = 1 }, + { name = "basalt crumbs", chance = 5794, maxCount = 1 }, + { name = "small topaz", chance = 5784, maxCount = 3 }, + { name = "amber staff", chance = 6240, maxCount = 1 }, + { id = 3039, chance = 8459, maxCount = 1 }, -- red gem +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1050 }, + { name = "combat", interval = 2600, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -1400, maxDamage = -1750, length = 8, spread = 3, effect = CONST_ME_HITBYFIRE, target = false }, + { name = "combat", interval = 3100, chance = 20, type = COMBAT_HOLYDAMAGE, minDamage = -1000, maxDamage = -1600, length = 8, spread = 3, effect = CONST_ME_HOLYAREA, target = false }, + { name = "combat", interval = 2600, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -1200, maxDamage = -1650, radius = 5, effect = CONST_ME_HITBYFIRE, target = true }, + { name = "largefirering", interval = 2000, chance = 10, minDamage = -800, maxDamage = -1400, target = false }, +} + +monster.defenses = { + defense = 120, + armor = 120, + mitigation = 3.04, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -15 }, + { type = COMBAT_ENERGYDAMAGE, percent = -10 }, + { type = COMBAT_EARTHDAMAGE, percent = 5 }, + { type = COMBAT_FIREDAMAGE, percent = 40 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 25 }, + { type = COMBAT_DEATHDAMAGE, percent = -20 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/darklight_matter.lua b/data-otxserver/monster/quests/rotten_blood/darklight_matter.lua new file mode 100644 index 000000000..fa2fae08e --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/darklight_matter.lua @@ -0,0 +1,127 @@ +local mType = Game.createMonsterType("Darklight Matter") +local monster = {} + +monster.description = "a darklight matter" +monster.experience = 22250 +monster.outfit = { + lookType = 1624, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2380 +monster.Bestiary = { + class = "Slime", + race = BESTY_RACE_SLIME, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Darklight Core.", +} + +monster.health = 30150 +monster.maxHealth = 30150 +monster.race = "venom" +monster.corpse = 43571 +monster.speed = 230 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 5000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 85, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = false, + canWalkOnFire = false, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "*twiggle*", yell = false }, + { text = "SSSSHRRR...", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 11755, maxCount = 1 }, + { name = "unstable darklight matter", chance = 9060, maxCount = 1 }, + { name = "darklight core", chance = 12887, maxCount = 1 }, + { name = "ultimate health potion", chance = 6553, maxCount = 6 }, + { id = 3039, chance = 1430 }, -- red gem + { name = "darklight matter", chance = 8849, maxCount = 1 }, + { name = "rubber cap", chance = 7180, maxCount = 1 }, + { id = 23544, chance = 3500, maxCount = 1 }, -- collar of red plasma + { name = "green gem", chance = 3500, maxCount = 1 }, + { name = "shadow sceptre", chance = 3500, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1100 }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_ENERGYDAMAGE, minDamage = -1400, maxDamage = -1800, radius = 5, effect = CONST_ME_PURPLESMOKE, target = true }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -1500, maxDamage = -1600, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = true }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_ENERGYDAMAGE, minDamage = -1400, maxDamage = -1650, length = 8, spread = 3, effect = CONST_ME_ELECTRICALSPARK, target = false }, + { name = "largeredring", interval = 2000, chance = 15, minDamage = -800, maxDamage = -1500, target = false }, +} + +monster.defenses = { + defense = 98, + armor = 98, + mitigation = 3.28, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -10 }, + { type = COMBAT_ENERGYDAMAGE, percent = 40 }, + { type = COMBAT_EARTHDAMAGE, percent = -10 }, + { type = COMBAT_FIREDAMAGE, percent = -25 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 20 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = false }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/darklight_source.lua b/data-otxserver/monster/quests/rotten_blood/darklight_source.lua new file mode 100644 index 000000000..b6e9c0653 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/darklight_source.lua @@ -0,0 +1,117 @@ +local mType = Game.createMonsterType("Darklight Source") +local monster = {} + +monster.description = "a darklight source" +monster.experience = 22465 +monster.outfit = { + lookType = 1660, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2398 +monster.Bestiary = { + class = "Magical", + race = BESTY_RACE_MAGICAL, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Darklight Core", +} + +monster.health = 31550 +monster.maxHealth = 31550 +monster.race = "undead" +monster.corpse = 43840 +monster.speed = 220 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.loot = { + { name = "crystal coin", chance = 5214, maxCount = 1 }, + { name = "yellow darklight matter", chance = 9397, maxCount = 1 }, + { name = "dark obsidian splinter", chance = 13215, maxCount = 1 }, + { name = "darklight core", chance = 7570, maxCount = 1 }, + { name = "small sapphire", chance = 5644, maxCount = 2 }, + { name = "blue gem", chance = 12909, maxCount = 1 }, + { name = "twiceslicer", chance = 11596, maxCount = 1 }, + { name = "white gem", chance = 13964, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1200 }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_ENERGYDAMAGE, minDamage = -1400, maxDamage = -1650, length = 8, spread = 3, effect = CONST_ME_BLUE_ENERGY_SPARK, target = false }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_HOLYDAMAGE, minDamage = -1500, maxDamage = -1600, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = false }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_ENERGYDAMAGE, minDamage = -1300, maxDamage = -1500, radius = 5, effect = CONST_ME_ELECTRICALSPARK, target = false }, +} + +monster.defenses = { + defense = 115, + armor = 115, + mitigation = 3.19, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -15 }, + { type = COMBAT_ENERGYDAMAGE, percent = 55 }, + { type = COMBAT_EARTHDAMAGE, percent = -10 }, + { type = COMBAT_FIREDAMAGE, percent = -15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 40 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/darklight_striker.lua b/data-otxserver/monster/quests/rotten_blood/darklight_striker.lua new file mode 100644 index 000000000..4d6a19d6d --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/darklight_striker.lua @@ -0,0 +1,120 @@ +local mType = Game.createMonsterType("Darklight Striker") +local monster = {} + +monster.description = "a darklight striker" +monster.experience = 22200 +monster.outfit = { + lookType = 1661, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2399 +monster.Bestiary = { + class = "Magical", + race = BESTY_RACE_MAGICAL, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Darklight Core", +} + +monster.health = 29700 +monster.maxHealth = 29700 +monster.race = "undead" +monster.corpse = 43844 +monster.speed = 210 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.loot = { + { name = "crystal coin", chance = 14380, maxCount = 1 }, + { name = "unstable darklight matter", chance = 14492, maxCount = 1 }, + { name = "darklight core", chance = 9783, maxCount = 1 }, + { name = "small topaz", chance = 11140, maxCount = 3 }, + { name = "ice rapier", chance = 5104, maxCount = 1 }, + { name = "dark obsidian splinter", chance = 14185, maxCount = 1 }, + { name = "blue gem", chance = 7355, maxCount = 1 }, + { name = "crystal mace", chance = 8812, maxCount = 1 }, + { name = "zaoan helmet", chance = 5572, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2500, chance = 100, minDamage = 0, maxDamage = -1100 }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_ENERGYDAMAGE, minDamage = -1400, maxDamage = -1650, length = 8, spread = 3, effect = CONST_ME_ELECTRICALSPARK, target = false }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_HOLYDAMAGE, minDamage = -1100, maxDamage = -1600, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = false }, + { name = "combat", interval = 2500, chance = 10, type = COMBAT_ENERGYDAMAGE, minDamage = -1300, maxDamage = -1500, radius = 5, effect = CONST_ME_ELECTRICALSPARK, target = false }, + { name = "extended holy chain", interval = 2000, chance = 15, minDamage = -800, maxDamage = -1200 }, + { name = "largepinkring", interval = 2500, chance = 10, minDamage = -1500, maxDamage = -1900, target = false }, +} + +monster.defenses = { + defense = 112, + armor = 112, + mitigation = 3.10, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 10 }, + { type = COMBAT_ENERGYDAMAGE, percent = 35 }, + { type = COMBAT_EARTHDAMAGE, percent = -15 }, + { type = COMBAT_FIREDAMAGE, percent = -25 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 30 }, + { type = COMBAT_HOLYDAMAGE, percent = 10 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/echo_of_chagorz.lua b/data-otxserver/monster/quests/rotten_blood/echo_of_chagorz.lua new file mode 100644 index 000000000..3627ffe75 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/echo_of_chagorz.lua @@ -0,0 +1,101 @@ +local mType = Game.createMonsterType("Echo Of Chagorz") +local monster = {} + +monster.description = "an echo of Chagorz" +monster.experience = 0 +monster.outfit = { + lookType = 1670, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 90000 +monster.maxHealth = 90000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 100 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 2500, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -750, maxDamage = -1750 }, + { name = "combat", interval = 2700, chance = 37, type = COMBAT_FIREDAMAGE, minDamage = -950, maxDamage = -2000, length = 8, spread = 3, effect = CONST_ME_HITBYFIRE, target = false }, + { name = "combat", interval = 3300, chance = 37, type = COMBAT_PHYSICALDAMAGE, minDamage = -1100, maxDamage = -1600, length = 8, spread = 0, effect = CONST_ME_EXPLOSIONHIT, target = false }, +} + +monster.defenses = { + defense = 65, + armor = 0, + mitigation = 2.0, + { name = "combat", interval = 3000, chance = 35, type = COMBAT_HEALING, minDamage = 400, maxDamage = 500, effect = CONST_ME_MAGIC_BLUE, target = false }, + { name = "speed", interval = 2000, chance = 15, speedChange = 320, effect = CONST_ME_MAGIC_RED, target = false, duration = 5000 }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/echo_of_ichgahal.lua b/data-otxserver/monster/quests/rotten_blood/echo_of_ichgahal.lua new file mode 100644 index 000000000..f35d2e139 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/echo_of_ichgahal.lua @@ -0,0 +1,101 @@ +local mType = Game.createMonsterType("Echo Of Ichgahal") +local monster = {} + +monster.description = "an echo of Ichgahal" +monster.experience = 0 +monster.outfit = { + lookType = 1669, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 90000 +monster.maxHealth = 90000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 100 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 2500, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -750, maxDamage = -1750 }, + { name = "combat", interval = 2700, chance = 37, type = COMBAT_FIREDAMAGE, minDamage = -950, maxDamage = -2000, length = 8, spread = 3, effect = CONST_ME_HITBYFIRE, target = false }, + { name = "combat", interval = 3300, chance = 37, type = COMBAT_PHYSICALDAMAGE, minDamage = -1100, maxDamage = -1600, length = 8, spread = 0, effect = CONST_ME_EXPLOSIONHIT, target = false }, +} + +monster.defenses = { + defense = 65, + armor = 0, + mitigation = 2.0, + { name = "combat", interval = 3000, chance = 35, type = COMBAT_HEALING, minDamage = 400, maxDamage = 500, effect = CONST_ME_MAGIC_BLUE, target = false }, + { name = "speed", interval = 2000, chance = 15, speedChange = 320, effect = CONST_ME_MAGIC_RED, target = false, duration = 5000 }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/echo_of_murcion.lua b/data-otxserver/monster/quests/rotten_blood/echo_of_murcion.lua new file mode 100644 index 000000000..83a1b002d --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/echo_of_murcion.lua @@ -0,0 +1,101 @@ +local mType = Game.createMonsterType("Echo Of Murcion") +local monster = {} + +monster.description = "an echo of Murcion" +monster.experience = 0 +monster.outfit = { + lookType = 1669, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 90000 +monster.maxHealth = 90000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 100 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 2500, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -750, maxDamage = -1750 }, + { name = "combat", interval = 2700, chance = 37, type = COMBAT_FIREDAMAGE, minDamage = -950, maxDamage = -2000, length = 8, spread = 0, effect = CONST_ME_HITBYFIRE, target = false }, + { name = "combat", interval = 3300, chance = 37, type = COMBAT_PHYSICALDAMAGE, minDamage = -1100, maxDamage = -1600, length = 8, spread = 0, effect = CONST_ME_EXPLOSIONHIT, target = false }, +} + +monster.defenses = { + defense = 65, + armor = 0, + mitigation = 2.0, + { name = "combat", interval = 3000, chance = 35, type = COMBAT_HEALING, minDamage = 400, maxDamage = 500, effect = CONST_ME_MAGIC_BLUE, target = false }, + { name = "speed", interval = 2000, chance = 15, speedChange = 320, effect = CONST_ME_MAGIC_RED, target = false, duration = 5000 }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/echo_of_vemiath.lua b/data-otxserver/monster/quests/rotten_blood/echo_of_vemiath.lua new file mode 100644 index 000000000..d865ba9ae --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/echo_of_vemiath.lua @@ -0,0 +1,101 @@ +local mType = Game.createMonsterType("Echo Of Vemiath") +local monster = {} + +monster.description = "an echo of Vemiath" +monster.experience = 0 +monster.outfit = { + lookType = 1670, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 90000 +monster.maxHealth = 90000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 100 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 2500, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -750, maxDamage = -1750 }, + { name = "combat", interval = 2700, chance = 37, type = COMBAT_FIREDAMAGE, minDamage = -950, maxDamage = -2000, length = 8, spread = 3, effect = CONST_ME_HITBYFIRE, target = false }, + { name = "combat", interval = 3300, chance = 37, type = COMBAT_PHYSICALDAMAGE, minDamage = -1100, maxDamage = -1600, length = 8, spread = 0, effect = CONST_ME_EXPLOSIONHIT, target = false }, +} + +monster.defenses = { + defense = 65, + armor = 0, + mitigation = 2.0, + { name = "combat", interval = 3000, chance = 35, type = COMBAT_HEALING, minDamage = 400, maxDamage = 500, effect = CONST_ME_MAGIC_BLUE, target = false }, + { name = "speed", interval = 2000, chance = 15, speedChange = 320, effect = CONST_ME_MAGIC_RED, target = false, duration = 5000 }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/elder_bloodjaw.lua b/data-otxserver/monster/quests/rotten_blood/elder_bloodjaw.lua new file mode 100644 index 000000000..37b638165 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/elder_bloodjaw.lua @@ -0,0 +1,102 @@ +local mType = Game.createMonsterType("Elder Bloodjaw") +local monster = {} + +monster.description = "an elder bloodjaw" +monster.experience = 0 +monster.outfit = { + lookType = 1628, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 86000 +monster.maxHealth = 86000 +monster.race = "undead" +monster.corpse = 43669 +monster.speed = 210 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 80, + health = 10, + damage = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "SHWAARR!", yell = false }, + { text = "SHWAARP!", yell = false }, +} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -490 }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_EARTHDAMAGE, minDamage = -220, maxDamage = -405, range = 7, radius = 1, shootEffect = CONST_ANI_POISON, target = true }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_LIFEDRAIN, minDamage = -65, maxDamage = -135, radius = 4, effect = CONST_ME_MAGIC_GREEN, target = false }, + { name = "drunk", interval = 2000, chance = 10, radius = 3, effect = CONST_ME_HITBYPOISON, target = false, duration = 5000 }, + { name = "blightwalker curse", interval = 2000, chance = 15, target = false }, + { name = "speed", interval = 2000, chance = 15, speedChange = -300, range = 7, shootEffect = CONST_ANI_POISON, target = true, duration = 30000 }, +} + +monster.defenses = { + defense = 100, + armor = 100, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -15 }, + { type = COMBAT_ENERGYDAMAGE, percent = -10 }, + { type = COMBAT_EARTHDAMAGE, percent = 5 }, + { type = COMBAT_FIREDAMAGE, percent = 40 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 25 }, + { type = COMBAT_DEATHDAMAGE, percent = -20 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/meandering_mushroom.lua b/data-otxserver/monster/quests/rotten_blood/meandering_mushroom.lua new file mode 100644 index 000000000..7dc04b953 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/meandering_mushroom.lua @@ -0,0 +1,121 @@ +local mType = Game.createMonsterType("Meandering Mushroom") +local monster = {} + +monster.description = "a meandering mushroom" +monster.experience = 21980 +monster.outfit = { + lookType = 1621, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2376 +monster.Bestiary = { + class = "Slime", + race = BESTY_RACE_SLIME, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Putrefactory.", +} + +monster.health = 29100 +monster.maxHealth = 29100 +monster.race = "undead" +monster.corpse = 43559 +monster.speed = 205 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 5000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 85, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = false, + canWalkOnFire = false, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.loot = { + { name = "crystal coin", chance = 11755, maxCount = 1 }, + { name = "lichen gobbler", chance = 9121, maxCount = 1 }, + { name = "white mushroom", chance = 12998, maxCount = 3 }, + { name = "rotten roots", chance = 9791, maxCount = 1 }, + { name = "wand of decay", chance = 14668, maxCount = 1 }, + { id = 3039, chance = 10406, maxCount = 1 }, -- red gem + { name = "worm sponge", chance = 10697, maxCount = 1 }, + { name = "dark mushroom", chance = 12313, maxCount = 3 }, + { name = "yellow gem", chance = 13520, maxCount = 1 }, + { name = "brown mushroom", chance = 6422, maxCount = 3 }, + { name = "terra amulet", chance = 13122, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1150 }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_DEATHDAMAGE, minDamage = -1800, maxDamage = -1900, radius = 5, effect = CONST_ME_MORTAREA, target = true }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_DEATHDAMAGE, minDamage = -1700, maxDamage = -1700, radius = 5, effect = CONST_ME_INSECTS, target = true }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_ICEDAMAGE, minDamage = -1100, maxDamage = -1300, length = 8, spread = 5, effect = CONST_ME_BLACKSMOKE, target = false }, + { name = "largeblackring", interval = 2000, chance = 10, minDamage = -900, maxDamage = -1500, target = false }, +} + +monster.defenses = { + defense = 115, + armor = 115, + mitigation = 3.19, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 25 }, + { type = COMBAT_EARTHDAMAGE, percent = -20 }, + { type = COMBAT_FIREDAMAGE, percent = -10 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 40 }, + { type = COMBAT_HOLYDAMAGE, percent = -15 }, + { type = COMBAT_DEATHDAMAGE, percent = 50 }, +} + +monster.immunities = { + { type = "paralyze", condition = false }, + { type = "outfit", condition = false }, + { type = "invisible", condition = false }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/mushroom.lua b/data-otxserver/monster/quests/rotten_blood/mushroom.lua new file mode 100644 index 000000000..d5611b8ea --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/mushroom.lua @@ -0,0 +1,94 @@ +local mType = Game.createMonsterType("Mushroom") +local monster = {} + +monster.description = "a Mushroom" +monster.experience = 0 +monster.outfit = { + lookType = 1669, --todo get correct lookType + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.health = 10000 +monster.maxHealth = 10000 +monster.race = "undead" +monster.corpse = 0 +monster.speed = 0 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 2500, + chance = 40, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 98, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = {} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -400 }, + { name = "combat", interval = 3000, chance = 100, type = COMBAT_LIFEDRAIN, minDamage = -2500, maxDamage = -3000, radius = 3, effect = CONST_ME_POISONAREA, target = false }, -- life drain bomb +} + +monster.defenses = { + defense = 65, + armor = 0, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = true }, + { type = "invisible", condition = true }, + { type = "bleed", condition = true }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/mycobiontic_beetle.lua b/data-otxserver/monster/quests/rotten_blood/mycobiontic_beetle.lua new file mode 100644 index 000000000..6a8267c9b --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/mycobiontic_beetle.lua @@ -0,0 +1,129 @@ +local mType = Game.createMonsterType("Mycobiontic Beetle") +local monster = {} + +monster.description = "a mycobiontic beetle" +monster.experience = 21175 +monster.outfit = { + lookType = 1620, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2375 +monster.Bestiary = { + class = "Vermin", + race = BESTY_RACE_VERMIN, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Jaded roots", +} + +monster.health = 30200 +monster.maxHealth = 30200 +monster.race = "undead" +monster.corpse = 43555 +monster.speed = 230 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 5000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.loot = { + { name = "crystal coin", chance = 15540 }, + { name = "ultimate health potion", chance = 43253, maxCount = 5 }, + { name = "serpent sword", chance = 32253 }, + { name = "glacier mask", chance = 21920 }, + { name = "small sapphire", chance = 34560, maxCount = 3 }, + { name = "organic acid", chance = 11678, maxCount = 1 }, + { name = "rotten roots", chance = 25920, maxCount = 1 }, + { name = "scarab coin", chance = 22920, maxCount = 3 }, + { name = "buckle", chance = 22920, maxCount = 1 }, + { name = "rotten vermin ichor", chance = 22920, maxCount = 1 }, + { name = "violet gem", chance = 18920 }, + { name = "blue gem", chance = 15920 }, + { name = "small ruby", chance = 24560, maxCount = 3 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1600 }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_EARTHDAMAGE, minDamage = -1100, maxDamage = -1400, radius = 5, effect = CONST_ME_GREEN_RINGS, target = true }, + { name = "combat", intervall = 2000, chance = 15, type = COMBAT_EARTHDAMAGE, minDamage = -1200, maxDamage = -1600, length = 8, spread = 3, effect = CONST_ME_GREEN_RINGS, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -1400, maxDamage = -1700, radius = 5, effect = CONST_ME_EXPLOSIONAREA, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_EARTHDAMAGE, minDamage = -1200, maxDamage = -1400, radius = 5, effect = CONST_ME_GROUNDSHAKER, target = false }, + { name = "largepoisonring", interval = 2000, chance = 10, minDamage = -900, maxDamage = -1300 }, +} + +monster.defenses = { + defense = 116, + armor = 116, + mitigation = 2.92, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 25 }, + { type = COMBAT_ENERGYDAMAGE, percent = -15 }, + { type = COMBAT_EARTHDAMAGE, percent = 60 }, + { type = COMBAT_FIREDAMAGE, percent = 35 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = -25 }, + { type = COMBAT_HOLYDAMAGE, percent = -5 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = false }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/oozing_carcass.lua b/data-otxserver/monster/quests/rotten_blood/oozing_carcass.lua new file mode 100644 index 000000000..bc44ba198 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/oozing_carcass.lua @@ -0,0 +1,123 @@ +local mType = Game.createMonsterType("Oozing Carcass") +local monster = {} + +monster.description = "an oozing carcass" +monster.experience = 20980 +monster.outfit = { + lookType = 1626, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2377 +monster.Bestiary = { + class = "Undead", + race = BESTY_RACE_UNDEAD, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Putrefactory.", +} + +monster.health = 27500 +monster.maxHealth = 27500 +monster.race = "undead" +monster.corpse = 43579 +monster.speed = 215 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 4, + color = 143, +} + +monster.loot = { + { name = "crystal coin", chance = 9000, maxCount = 1 }, + { name = "lichen gobbler", chance = 12369, maxCount = 1 }, + { name = "small emerald", chance = 12859, maxCount = 1 }, + { id = 3039, chance = 9808, maxCount = 1 }, -- red gem + { name = "skull staff", chance = 12316, maxCount = 1 }, + { name = "bone shield", chance = 6752, maxCount = 1 }, + { name = "yellow gem", chance = 8634, maxCount = 1 }, + { name = "rotten roots", chance = 13133, maxCount = 1 }, + { name = "decayed finger bone", chance = 6964, maxCount = 1 }, + { name = "ultimate health potion", chance = 10285, maxCount = 2 }, + { name = "bloody edge", chance = 12270, maxCount = 1 }, + { name = "spellbook of warding", chance = 5084, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -600 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_DEATHDAMAGE, minDamage = -1500, maxDamage = -1600, radius = 5, effect = CONST_ME_BLACKSMOKE, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_ICEDAMAGE, minDamage = -1400, maxDamage = -1500, radius = 5, effect = CONST_ME_ICEAREA, target = false }, + { name = "combat", interval = 2000, chance = 25, type = COMBAT_ICEDAMAGE, minDamage = -1400, maxDamage = -1550, length = 8, spread = 5, effect = CONST_ME_ICEAREA, target = false }, + { name = "largedeathring", interval = 2000, chance = 20, minDamage = -850, maxDamage = -1400, target = false }, + { name = "energy chain", interval = 3000, chance = 20, minDamage = -1050, maxDamage = -1400, target = false }, +} + +monster.defenses = { + defense = 102, + armor = 102, + mitigation = 3.10, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 25 }, + { type = COMBAT_EARTHDAMAGE, percent = -20 }, + { type = COMBAT_FIREDAMAGE, percent = -10 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 35 }, + { type = COMBAT_HOLYDAMAGE, percent = -25 }, + { type = COMBAT_DEATHDAMAGE, percent = 40 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = true }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/oozing_corpus.lua b/data-otxserver/monster/quests/rotten_blood/oozing_corpus.lua new file mode 100644 index 000000000..7408ed46e --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/oozing_corpus.lua @@ -0,0 +1,120 @@ +local mType = Game.createMonsterType("Oozing Corpus") +local monster = {} + +monster.description = "an oozing corpus" +monster.experience = 20600 +monster.outfit = { + lookType = 1625, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2381 +monster.Bestiary = { + class = "Undead", + race = BESTY_RACE_UNDEAD, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Jaded Roots.", +} + +monster.health = 28700 +monster.maxHealth = 28700 +monster.race = "undead" +monster.corpse = 43575 +monster.speed = 220 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 4, + color = 143, +} + +monster.loot = { + { name = "crystal coin", chance = 9000, maxCount = 1 }, + { name = "organic acid", chance = 7678, maxCount = 1 }, + { name = "terra boots", chance = 12369, maxCount = 1 }, + { name = "small amethyst", chance = 12859, maxCount = 1 }, + { name = "rotten roots", chance = 13133, maxCount = 1 }, + { name = "blue gem", chance = 9808, maxCount = 1 }, + { name = "dragonbone staff", chance = 6964, maxCount = 1 }, + { name = "worm sponge", chance = 7270, maxCount = 1 }, + { name = "violet gem", chance = 5084, maxCount = 1 }, + { name = "jade hammer", chance = 3073, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1600 }, + { name = "combat", interval = 2500, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -1300, maxDamage = -1700, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = true }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -1400, maxDamage = -1550, length = 8, spread = 3, effect = CONST_ME_GROUNDSHAKER, target = false }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_EARTHDAMAGE, minDamage = -1100, maxDamage = -1550, length = 8, spread = 3, effect = CONST_ME_GREEN_RINGS, target = false }, + { name = "death chain", interval = 3000, chance = 15, minDamage = -900, maxDamage = -1300, target = true }, +} + +monster.defenses = { + defense = 100, + armor = 107, + mitigation = 3.25, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 30 }, + { type = COMBAT_ENERGYDAMAGE, percent = -25 }, + { type = COMBAT_EARTHDAMAGE, percent = 40 }, + { type = COMBAT_FIREDAMAGE, percent = 25 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = -10 }, + { type = COMBAT_HOLYDAMAGE, percent = -10 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = true }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/rotten_man-maggot.lua b/data-otxserver/monster/quests/rotten_blood/rotten_man-maggot.lua new file mode 100644 index 000000000..19eb6d03b --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/rotten_man-maggot.lua @@ -0,0 +1,124 @@ +local mType = Game.createMonsterType("Rotten Man-Maggot") +local monster = {} + +monster.description = "a rotten man-maggot" +monster.experience = 22625 +monster.outfit = { + lookType = 1655, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2393 +monster.Bestiary = { + class = "Vermin", + race = BESTY_RACE_VERMIN, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Putrefactory.", +} + +monster.health = 31100 +monster.maxHealth = 31100 +monster.race = "undead" +monster.corpse = 43820 +monster.speed = 195 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 5000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = true, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, +} + +monster.loot = { + { name = "crystal coin", chance = 10340, maxCount = 1 }, + { name = "small amethyst", chance = 7364, maxCount = 2 }, + { name = "lichen gobbler", chance = 8391, maxCount = 1 }, + { name = "rotten roots", chance = 11619, maxCount = 1 }, + { id = 6299, chance = 12591, maxCount = 1 }, -- death ring + { name = "wood cape", chance = 14371, maxCount = 1 }, + { id = 3039, chance = 5155, maxCount = 1 }, -- red gem + { name = "yellow gem", chance = 9564, maxCount = 1 }, + { name = "blooded worm", chance = 5096, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -900 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_DEATHDAMAGE, minDamage = -1100, maxDamage = -1400, radius = 5, effect = CONST_ME_MORTAREA, target = true }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_ICEDAMAGE, minDamage = -1300, maxDamage = -1800, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = true }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_ICEDAMAGE, minDamage = -1200, maxDamage = -1700, length = 8, spread = 5, effect = CONST_ME_ICEAREA, target = false }, + { name = "largeicering", interval = 2000, chance = 15, minDamage = -800, maxDamage = -1200, target = false }, +} + +monster.defenses = { + defense = 110, + armor = 110, + mitigation = 2.75, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 55 }, + { type = COMBAT_EARTHDAMAGE, percent = -15 }, + { type = COMBAT_FIREDAMAGE, percent = -10 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 40 }, + { type = COMBAT_HOLYDAMAGE, percent = -15 }, + { type = COMBAT_DEATHDAMAGE, percent = 30 }, +} + +monster.immunities = { + { type = "paralyze", condition = false }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/sopping_carcass.lua b/data-otxserver/monster/quests/rotten_blood/sopping_carcass.lua new file mode 100644 index 000000000..26458dfe2 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/sopping_carcass.lua @@ -0,0 +1,110 @@ +local mType = Game.createMonsterType("Sopping Carcass") +local monster = {} + +monster.description = "a sopping carcass" +monster.experience = 23425 +monster.outfit = { + lookType = 1658, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2396 +monster.Bestiary = { + class = "Undead", + race = BESTY_RACE_UNDEAD, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Putrefactory.", +} + +monster.health = 32700 +monster.maxHealth = 32700 +monster.race = "undead" +monster.corpse = 43832 +monster.speed = 210 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1100 }, + { name = "combat", interval = 2000, chance = 24, type = COMBAT_DEATHDAMAGE, minDamage = -1400, maxDamage = -1500, radius = 5, effect = CONST_ME_MORTAREA, target = false }, + { name = "combat", interval = 2500, chance = 15, type = COMBAT_ICEDAMAGE, minDamage = -1200, maxDamage = -1400, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = false }, + { name = "combat", interval = 2000, chance = 25, type = COMBAT_EARTHDAMAGE, minDamage = -900, maxDamage = -1400, radius = 5, effect = CONST_ME_BIGPLANTS, target = false }, + { name = "combat", interval = 2000, chance = 25, type = COMBAT_DEATHDAMAGE, minDamage = -1100, maxDamage = -1550, length = 8, spread = 5, effect = CONST_ME_BLACKSMOKE, target = false }, + { name = "ice chain", interval = 3000, chance = 15, minDamage = -1200, maxDamage = -1500, target = false }, +} + +monster.defenses = { + defense = 109, + armor = 109, + mitigation = 3.28, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 35 }, + { type = COMBAT_EARTHDAMAGE, percent = -15 }, + { type = COMBAT_FIREDAMAGE, percent = -5 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 50 }, + { type = COMBAT_HOLYDAMAGE, percent = -20 }, + { type = COMBAT_DEATHDAMAGE, percent = 60 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = true }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/sopping_corpus.lua b/data-otxserver/monster/quests/rotten_blood/sopping_corpus.lua new file mode 100644 index 000000000..16cc74d32 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/sopping_corpus.lua @@ -0,0 +1,130 @@ +local mType = Game.createMonsterType("Sopping Corpus") +local monster = {} + +monster.description = "a sopping corpus" +monster.experience = 22465 +monster.outfit = { + lookType = 1659, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2397 +monster.Bestiary = { + class = "Undead", + race = BESTY_RACE_UNDEAD, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Jaded Roots.", +} + +monster.health = 33400 +monster.maxHealth = 33400 +monster.race = "undead" +monster.corpse = 43836 +monster.speed = 210 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 0, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = false, + canPushItems = true, + canPushCreatures = false, + staticAttackChance = 90, + targetDistance = 0, + runHealth = 0, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = true, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "*Lessshhh!*", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 42860 }, + { name = "ultimate mana potion", chance = 42860, minCount = 2, maxCount = 3 }, + { id = 7385, chance = 14290 }, -- crimson sword + { name = "ultimate health potion", chance = 14290, maxCount = 2 }, + { name = "organic acid", chance = 7678, maxCount = 1 }, + { name = "rotten roots", chance = 13133, maxCount = 1 }, + { name = "emerald bangle", chance = 8558, maxCount = 1 }, + { name = "underworld rod", chance = 8380, maxCount = 1 }, + { name = "violet gem", chance = 5084, maxCount = 1 }, + { name = "blue gem", chance = 9808, maxCount = 1 }, + { name = "relic sword", chance = 6964, maxCount = 1 }, + { name = "skullcracker armor", chance = 7270, maxCount = 1 }, + { id = 23531, chance = 3073, maxCount = 1 }, -- ring of green plasma +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1600 }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_PHYSICALDAMAGE, minDamage = -1300, maxDamage = -1600, length = 8, spread = 3, effect = CONST_ME_GROUNDSHAKER, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_PHYSICALDAMAGE, minDamage = -1200, maxDamage = -1500, effect = CONST_ME_BIG_SCRATCH, target = true }, + { name = "combat", interval = 2000, chance = 15, type = COMBAT_PHYSICALDAMAGE, minDamage = -1400, maxDamage = -1600, radius = 5, effect = CONST_ME_GROUNDSHAKER, target = true }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_EARTHDAMAGE, minDamage = -1200, maxDamage = -1500, length = 8, spread = 3, effect = CONST_ME_GREEN_RINGS, target = false }, + { name = "largepoisonring", interval = 2000, chance = 10, minDamage = -1000, maxDamage = -1200, target = false }, +} + +monster.defenses = { + defense = 112, + armor = 112, + mitigation = 3.25, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 40 }, + { type = COMBAT_ENERGYDAMAGE, percent = -20 }, + { type = COMBAT_EARTHDAMAGE, percent = 50 }, + { type = COMBAT_FIREDAMAGE, percent = 30 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = -10 }, + { type = COMBAT_HOLYDAMAGE, percent = 5 }, + { type = COMBAT_DEATHDAMAGE, percent = 10 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = true }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/walking_pillar.lua b/data-otxserver/monster/quests/rotten_blood/walking_pillar.lua new file mode 100644 index 000000000..89e1e62a1 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/walking_pillar.lua @@ -0,0 +1,130 @@ +local mType = Game.createMonsterType("Walking Pillar") +local monster = {} + +monster.description = "a walking pillar" +monster.experience = 24300 +monster.outfit = { + lookType = 1656, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2394 +monster.Bestiary = { + class = "Construct", + race = BESTY_RACE_CONSTRUCT, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Darklight Core", +} + +monster.health = 38000 +monster.maxHealth = 38000 +monster.race = "undead" +monster.corpse = 43824 +monster.speed = 190 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = true, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 50, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = false, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "TREEMBLE", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 12186, maxCount = 1 }, + { name = "yellow darklight matter", chance = 5354, maxCount = 1 }, + { name = "magma clump", chance = 11440, maxCount = 1 }, + { name = "darklight core", chance = 10276, maxCount = 1 }, + { id = 12600, chance = 8489, maxCount = 4 }, -- coal + { name = "darklight basalt chunk", chance = 12855, maxCount = 1 }, + { name = "onyx chip", chance = 12831, maxCount = 2 }, + { name = "strange helmet", chance = 11001, maxCount = 1 }, + { name = "fire sword", chance = 8347, maxCount = 1 }, + { name = "ultimate mana potion", chance = 9687, maxCount = 3 }, + { name = "blue gem", chance = 5868, maxCount = 1 }, + { name = "magma legs", chance = 14497, maxCount = 1 }, + { name = "white gem", chance = 9936, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1500 }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_ENERGYDAMAGE, minDamage = -1400, maxDamage = -1650, length = 8, spread = 3, effect = CONST_ME_BLUE_ENERGY_SPARK, target = false }, + { name = "combat", intervall = 2000, chance = 20, type = COMBAT_HOLYDAMAGE, minDamage = -1500, maxDamage = -1800, radius = 5, effect = CONST_ME_PURPLESMOKE, target = true }, + { name = "combat", interval = 2000, chance = 10, type = COMBAT_HOLYDAMAGE, minDamage = -1200, maxDamage = -1200, radius = 5, effect = CONST_ME_GHOSTLY_BITE, target = true }, + { name = "extended energy chain", interval = 2000, chance = 5, minDamage = -800, maxDamage = 1200, target = true }, + { name = "largepinkring", interval = 3500, chance = 10, minDamage = -1100, maxDamage = -1600, target = false }, +} + +monster.defenses = { + defense = 120, + armor = 120, + mitigation = 2.75, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -10 }, + { type = COMBAT_ENERGYDAMAGE, percent = 60 }, + { type = COMBAT_EARTHDAMAGE, percent = -15 }, + { type = COMBAT_FIREDAMAGE, percent = -15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 45 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 10 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/monster/quests/rotten_blood/wandering_pillar.lua b/data-otxserver/monster/quests/rotten_blood/wandering_pillar.lua new file mode 100644 index 000000000..e6cfb42a3 --- /dev/null +++ b/data-otxserver/monster/quests/rotten_blood/wandering_pillar.lua @@ -0,0 +1,133 @@ +local mType = Game.createMonsterType("Wandering Pillar") +local monster = {} + +monster.description = "a wandering pillar" +monster.experience = 23200 +monster.outfit = { + lookType = 1657, + lookHead = 0, + lookBody = 0, + lookLegs = 0, + lookFeet = 0, + lookAddons = 0, + lookMount = 0, +} + +monster.raceId = 2395 +monster.Bestiary = { + class = "Construct", + race = BESTY_RACE_CONSTRUCT, + toKill = 5000, + FirstUnlock = 200, + SecondUnlock = 2000, + CharmsPoints = 100, + Stars = 5, + Occurrence = 0, + Locations = "Darklight Core", +} + +monster.health = 37000 +monster.maxHealth = 37000 +monster.race = "undead" +monster.corpse = 43828 +monster.speed = 190 +monster.manaCost = 0 + +monster.changeTarget = { + interval = 4000, + chance = 10, +} + +monster.strategiesTarget = { + nearest = 70, + health = 10, + damage = 10, + random = 10, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + pushable = false, + rewardBoss = false, + illusionable = true, + canPushItems = true, + canPushCreatures = true, + staticAttackChance = 90, + targetDistance = 1, + runHealth = 50, + healthHidden = false, + isBlockable = false, + canWalkOnEnergy = true, + canWalkOnFire = false, + canWalkOnPoison = true, +} + +monster.light = { + level = 0, + color = 0, +} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "POWERRR!!", yell = false }, + { text = "DARKNESS. DEATH. ENERGIES.", yell = false }, + { text = "TREMMMBLE!", yell = false }, +} + +monster.loot = { + { name = "crystal coin", chance = 6629, maxCount = 1 }, + { name = "darklight obsidian axe", chance = 14652, maxCount = 1 }, + { name = "basalt crumbs", chance = 8184, maxCount = 1 }, + { name = "sulphurous stone", chance = 5873, maxCount = 1 }, + { name = "magma boots", chance = 5080, maxCount = 1 }, + { id = 12600, chance = 9802, maxCount = 4 }, -- coal + { name = "dark helmet", chance = 7490, maxCount = 1 }, + { name = "magma coat", chance = 11753, maxCount = 1 }, + { name = "onyx chip", chance = 9311, maxCount = 2 }, + { name = "darklight core", chance = 5957, maxCount = 1 }, + { name = "fire sword", chance = 8319, maxCount = 1 }, + { name = "magma clump", chance = 6260, maxCount = 1 }, + { id = 3039, chance = 9915, maxCount = 1 }, -- red gem + { name = "green gem", chance = 12864, maxCount = 1 }, + { name = "basalt core", chance = 9037, maxCount = 1 }, +} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = 0, maxDamage = -1200 }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_ENERGYDAMAGE, minDamage = -1400, maxDamage = -1650, length = 8, spread = 3, effect = CONST_ME_BLUE_ENERGY_SPARK, target = false }, + { name = "combat", intervall = 2000, chance = 20, type = COMBAT_ENERGYDAMAGE, minDamage = -1100, maxDamage = -1500, radius = 5, effect = CONST_ME_PINK_BEAM, target = false }, + { name = "combat", interval = 2000, chance = 20, type = COMBAT_FIREDAMAGE, minDamage = -1400, maxDamage = -1700, radius = 5, effect = CONST_ME_HITBYFIRE, target = true }, + { name = "largeholyring", interval = 3000, chance = 15, minDamage = -900, maxDamage = -1250 }, +} + +monster.defenses = { + defense = 120, + armor = 120, + mitigation = 2.75, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = -10 }, + { type = COMBAT_ENERGYDAMAGE, percent = 60 }, + { type = COMBAT_EARTHDAMAGE, percent = -15 }, + { type = COMBAT_FIREDAMAGE, percent = -15 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 45 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 10 }, +} + +monster.immunities = { + { type = "paralyze", condition = true }, + { type = "outfit", condition = false }, + { type = "invisible", condition = true }, + { type = "bleed", condition = false }, +} + +mType:register(monster) diff --git a/data-otxserver/scripts/actions/object/imbuement_shrine.lua b/data-otxserver/scripts/actions/object/imbuement_shrine.lua index f5d878df2..5cec0f750 100644 --- a/data-otxserver/scripts/actions/object/imbuement_shrine.lua +++ b/data-otxserver/scripts/actions/object/imbuement_shrine.lua @@ -5,7 +5,7 @@ function imbuement.onUse(player, item, fromPosition, target, toPosition, isHotke return player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You did not collect enough knowledge from the ancient Shapers. Visit the Shaper temple in Thais for help.") end - if not target or not (target:isItem()) then + if type(target) ~= "userdata" or not target:isItem() then return player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You can only use the shrine on an valid item.") end diff --git a/data-otxserver/scripts/creaturescripts/monster/spawn_system.lua b/data-otxserver/scripts/creaturescripts/monster/spawn_system.lua deleted file mode 100644 index 580dba6d0..000000000 --- a/data-otxserver/scripts/creaturescripts/monster/spawn_system.lua +++ /dev/null @@ -1,25 +0,0 @@ -local monsterDeath = CreatureEvent("monsterDeath") -function monsterDeath.onDeath(creature, corpse, killer, mostDamage, unjustified, mostDamageUnjustified) - if creature and creature:isMonster() then - local self = creature:getStorageValue(MonsterStorage.Spawn.monster_spawn_object) - self:executeFunctionMonster("onDeath", creature) - self:deleteMonster(creature) - return true - end - return true -end - -monsterDeath:register() - -local monsterDeathBoss = CreatureEvent("monsterDeathBoss") -function monsterDeathBoss.onDeath(creature, corpse, killer, mostDamage, unjustified, mostDamageUnjustified) - if creature and creature:isMonster() then - local self = creature:getStorageValue(MonsterStorage.Spawn.monster_spawn_object) - self:removeSpawn() - self:removeMonsters() - return true - end - return true -end - -monsterDeathBoss:register() diff --git a/data-otxserver/scripts/creaturescripts/others/player_death.lua b/data-otxserver/scripts/creaturescripts/others/player_death.lua deleted file mode 100644 index 2eacfccf4..000000000 --- a/data-otxserver/scripts/creaturescripts/others/player_death.lua +++ /dev/null @@ -1,142 +0,0 @@ -local deathListEnabled = true - -local playerDeath = CreatureEvent("PlayerDeath") -function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustified, mostDamageUnjustified) - if not deathListEnabled then - return - end - - local byPlayer = 0 - local killerName - if killer ~= nil then - if killer:isPlayer() then - byPlayer = 1 - else - local master = killer:getMaster() - if master and master ~= killer and master:isPlayer() then - killer = master - byPlayer = 1 - end - end - killerName = killer:isMonster() and killer:getType():getNameDescription() or killer:getName() - else - killerName = "field item" - end - - local byPlayerMostDamage = 0 - local mostDamageKillerName - if mostDamageKiller ~= nil then - if mostDamageKiller:isPlayer() then - byPlayerMostDamage = 1 - else - local master = mostDamageKiller:getMaster() - if master and master ~= mostDamageKiller and master:isPlayer() then - mostDamageKiller = master - byPlayerMostDamage = 1 - end - end - mostDamageName = mostDamageKiller:isMonster() and mostDamageKiller:getType():getNameDescription() or mostDamageKiller:getName() - else - mostDamageName = "field item" - end - - player:takeScreenshot(byPlayer and SCREENSHOT_TYPE_DEATHPVP or SCREENSHOT_TYPE_DEATHPVE) - - if mostDamageKiller and mostDamageKiller:isPlayer() then - mostDamageKiller:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) - end - - local playerGuid = player:getGuid() - db.query( - "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES (" - .. playerGuid - .. ", " - .. os.time() - .. ", " - .. player:getLevel() - .. ", " - .. db.escapeString(killerName) - .. ", " - .. byPlayer - .. ", " - .. db.escapeString(mostDamageName) - .. ", " - .. byPlayerMostDamage - .. ", " - .. (unjustified and 1 or 0) - .. ", " - .. (mostDamageUnjustified and 1 or 0) - .. ")" - ) - local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid) - -- Start Webhook Player Death - Webhook.sendMessage(":skull_crossbones: " .. player:getMarkdownLink() .. " has died. Killed at level _" .. player:getLevel() .. "_ by **" .. killerName .. "**.", announcementChannels["player-kills"]) - -- End Webhook Player Death - - local deathRecords = 0 - local tmpResultId = resultId - while tmpResultId ~= false do - tmpResultId = Result.next(resultId) - deathRecords = deathRecords + 1 - end - - if resultId ~= false then - Result.free(resultId) - end - - if byPlayer == 1 then - killer:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) - local targetGuild = player:getGuild() - local targetGuildId = targetGuild and targetGuild:getId() or 0 - if targetGuildId ~= 0 then - local killerGuild = killer:getGuild() - local killerGuildId = killerGuild and killerGuild:getId() or 0 - if killerGuildId ~= 0 and targetGuildId ~= killerGuildId and isInWar(player:getId(), killer:getId()) then - local warId = false - resultId = db.storeQuery("SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND \z - ((`guild1` = " .. killerGuildId .. " AND `guild2` = " .. targetGuildId .. ") OR \z - (`guild1` = " .. targetGuildId .. " AND `guild2` = " .. killerGuildId .. "))") - if resultId then - warId = Result.getNumber(resultId, "id") - Result.free(resultId) - end - - if warId then - local playerName = player:getName() - db.asyncQuery("INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) \z - VALUES (" .. db.escapeString(killerName) .. ", " .. db.escapeString(playerName) .. ", " .. killerGuildId .. ", \z - " .. targetGuildId .. ", " .. os.time() .. ", " .. warId .. ")") - - resultId = db.storeQuery("SELECT `guild_wars`.`id`, `guild_wars`.`frags_limit`, (SELECT COUNT(1) FROM `guildwar_kills` \z - WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild1`) AS guild1_kills, \z - (SELECT COUNT(1) FROM `guildwar_kills` WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild2`) AS guild2_kills \z - FROM `guild_wars` WHERE (`guild1` = " .. killerGuildId .. " OR `guild2` = " .. killerGuildId .. ") AND `status` = 1 AND `id` = " .. warId) - - if resultId then - local guild1_kills = Result.getNumber(resultId, "guild1_kills") - local guild2_kills = Result.getNumber(resultId, "guild2_kills") - local frags_limit = Result.getNumber(resultId, "frags_limit") - Result.free(resultId) - - local members = killerGuild:getMembersOnline() - for i = 1, #members do - members[i]:sendChannelMessage(members[i], string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1_kills, guild2_kills, killerGuild:getName(), frags_limit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) - end - - local enemyMembers = targetGuild:getMembersOnline() - for i = 1, #enemyMembers do - enemyMembers[i]:sendChannelMessage(enemyMembers[i], string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1_kills, guild2_kills, killerGuild:getName(), frags_limit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) - end - - if guild1_kills >= frags_limit or guild2_kills >= frags_limit then - db.query("UPDATE `guild_wars` SET `status` = 4, `ended` = " .. os.time() .. " WHERE `status` = 1 AND `id` = " .. warId) - Game.broadcastMessage(string.format("%s has just won the war against %s.", killerGuild:getName(), targetGuild:getName())) - end - end - end - end - end - end -end - -playerDeath:register() diff --git a/data-otxserver/scripts/lib/monster_functions.lua b/data-otxserver/scripts/lib/monster_functions.lua index cdb56c5e1..0b362de27 100644 --- a/data-otxserver/scripts/lib/monster_functions.lua +++ b/data-otxserver/scripts/lib/monster_functions.lua @@ -1,7 +1,7 @@ function Monster:handleCobraOnSpawn() if Game.getStorageValue(Global.Storage.CobraFlask) >= os.time() then - monster:setHealth(monster:getMaxHealth() * 0.75) - monster:getPosition():sendMagicEffect(CONST_ME_GREEN_RINGS) + self:setHealth(self:getMaxHealth() * 0.75) + self:getPosition():sendMagicEffect(CONST_ME_GREEN_RINGS) else Game.setStorageValue(Global.Storage.CobraFlask, -1) end diff --git a/data-otxserver/scripts/lib/register_actions.lua b/data-otxserver/scripts/lib/register_actions.lua index a65de89e0..48ef4c389 100644 --- a/data-otxserver/scripts/lib/register_actions.lua +++ b/data-otxserver/scripts/lib/register_actions.lua @@ -242,7 +242,7 @@ local function addFerumbrasAscendantReward(player, target, toPosition) end function onDestroyItem(player, item, fromPosition, target, toPosition, isHotkey) - if not target or target == nil or type(target) ~= "userdata" or not target:isItem() then + if not target or type(target) ~= "userdata" or not target:isItem() then return false end @@ -651,6 +651,7 @@ function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) --The Ice Islands Quest, Nibelor 1: Breaking the Ice local missionProgress = player:getStorageValue(Storage.Quest.U8_0.TheIceIslands.Mission02) local pickAmount = player:getStorageValue(Storage.Quest.U8_0.TheIceIslands.PickAmount) + if missionProgress < 1 or pickAmount >= 3 or player:getStorageValue(Storage.Quest.U8_0.TheIceIslands.Questline) ~= 3 then return false end @@ -704,7 +705,10 @@ function onUsePick(player, item, fromPosition, target, toPosition, isHotkey) -- The Pits of Inferno Quest if toPosition == Position(32808, 32334, 11) then for i = 1, #lava do - Game.createItem(5815, 1, lava[i]) + local lavaTile = Tile(lava[i]):getItemById(21477) + if lavaTile then + lavaTile:transform(5815) + end end target:transform(3141) toPosition:sendMagicEffect(CONST_ME_SMOKE) diff --git a/data-otxserver/scripts/spells/monster/time_guardiann.lua b/data-otxserver/scripts/spells/monster/time_guardiann.lua index f98ee4195..9e4ffe6ae 100644 --- a/data-otxserver/scripts/spells/monster/time_guardiann.lua +++ b/data-otxserver/scripts/spells/monster/time_guardiann.lua @@ -3,51 +3,94 @@ local monsters = { [2] = { pos = Position(32815, 32664, 14) }, } -local function functionBack(position, oldpos) - local guardian = Tile(position):getTopCreature() - local bool, diference, health = false, 0, 0 - local spectators, spectator = Game.getSpectators(Position(32813, 32664, 14), false, false, 15, 15, 15, 15) - for v = 1, #spectators do - spectator = spectators[v] - if spectator:getName():lower() == "the blazing time guardian" or spectator:getName():lower() == "the freezing time guardian" then - oldpos = spectator:getPosition() - bool = true +local function functionBack(pos, oldPos) + local position = Position(pos) + if not position then + return + end + + local tile = Tile(position) + if not tile then + return + end + + local guardian = tile:getTopCreature() + if not guardian then + return + end + + local haveGuardianMonster = false + local spectator1 = nil + + local spectators1 = Game.getSpectators(Position(32813, 32664, 14), false, false, 15, 15, 15, 15) + for index = 1, #spectators1 do + spectator1 = spectators1[index] + if spectator1 then + if spectator1:isMonster() and spectator1:getName():lower() == "the blazing time guardian" or spectator1:getName():lower() == "the freezing time guardian" then + oldPos = spectator1:getPosition() + haveGuardianMonster = true + end end end - if not bool then + + if not haveGuardianMonster then guardian:remove() return true end - local specs, spec = Game.getSpectators(Position(32813, 32664, 14), false, false, 15, 15, 15, 15) - for i = 1, #specs do - spec = specs[i] - if spec:isMonster() and spec:getName():lower() == "the blazing time guardian" or spec:getName():lower() == "the freezing time guardian" then - spec:teleportTo(position) - health = spec:getHealth() - diference = guardian:getHealth() - health + + local diference = 0 + local spectator = nil + + local spectators2, spectator2 = Game.getSpectators(Position(32813, 32664, 14), false, false, 15, 15, 15, 15) + for i = 1, #spectators2 do + spectator2 = spectators2[i] + if spectator2 then + if spectator2:isMonster() and spectator2:getName():lower() == "the blazing time guardian" or spectator2:getName():lower() == "the freezing time guardian" then + spectator2:teleportTo(position) + diference = guardian:getHealth() - spectator2:getHealth() + end end end - guardian:addHealth(-diference) - guardian:teleportTo(oldpos) + + if diference > 0 then + guardian:addHealth(-diference) + end + + guardian:teleportTo(oldPos) end local spell = Spell("instant") function spell.onCastSpell(creature, var) - local index = math.random(1, 2) local monsterPos = creature:getPosition() if monsterPos.z ~= 14 then return true end + + local index = math.random(1, 2) local position = monsters[index].pos - local form = Tile(position):getTopCreature() - creature:teleportTo(position) - local diference, health = 0, 0 - health = creature:getHealth() - diference = form:getHealth() - health - form:addHealth(-diference) - form:teleportTo(monsterPos) - addEvent(functionBack, 30 * 1000, position, monsterPos) + if position then + local tile = Tile(position) + if not tile then + return true + end + + local form = tile:getTopCreature() + if not form then + return true + end + + creature:teleportTo(position) + + local diference = form:getHealth() - creature:getHealth() + if diference and diference > 0 then + form:addHealth(-diference) + end + + form:teleportTo(monsterPos) + addEvent(functionBack, 30 * 1000, position, monsterPos) + end + return true end diff --git a/data/events/scripts/party.lua b/data/events/scripts/party.lua index b27965994..b94613c07 100644 --- a/data/events/scripts/party.lua +++ b/data/events/scripts/party.lua @@ -66,25 +66,14 @@ function Party:onDisband() end function Party:onShareExperience(exp) - local sharedExperienceMultiplier = 1.20 --20% - local vocationsIds = {} + local uniqueVocationsCount = self:getUniqueVocationsCount() + local partySize = self:getMemberCount() + 1 - local vocationId = self:getLeader():getVocation():getBase():getId() - if vocationId ~= VOCATION_NONE then - table.insert(vocationsIds, vocationId) - end - - for _, member in ipairs(self:getMembers()) do - vocationId = member:getVocation():getBase():getId() - if not table.contains(vocationsIds, vocationId) and vocationId ~= VOCATION_NONE then - table.insert(vocationsIds, vocationId) - end - end - - local size = #vocationsIds - if size > 1 then - sharedExperienceMultiplier = 1.0 + ((size * (5 * (size - 1) + 10)) / 100) - end + -- Formula to calculate the % based on the vocations amount + local sharedExperienceMultiplier = ((0.1 * (uniqueVocationsCount ^ 2)) - (0.2 * uniqueVocationsCount) + 1.3) + -- Since the formula its non linear, we need to subtract 0.1 if all vocations are present, + -- because on all vocations the multiplier is 2.1 and it should be 2.0 + sharedExperienceMultiplier = partySize < 4 and sharedExperienceMultiplier or sharedExperienceMultiplier - 0.1 - return math.ceil((exp * sharedExperienceMultiplier) / (#self:getMembers() + 1)) + return math.ceil((exp * sharedExperienceMultiplier) / partySize) end diff --git a/data/events/scripts/player.lua b/data/events/scripts/player.lua index 253f30c2f..0b576013e 100644 --- a/data/events/scripts/player.lua +++ b/data/events/scripts/player.lua @@ -275,12 +275,18 @@ function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, return true end - -- Bath tube local toTile = Tile(toCylinder:getPosition()) if toTile then local topDownItem = toTile:getTopDownItem() - if topDownItem and table.contains({ BATHTUB_EMPTY, BATHTUB_FILLED }, topDownItem:getId()) then - return false + if topDownItem then + local topDownItemItemId = topDownItem:getId() + if table.contains({ BATHTUB_EMPTY, BATHTUB_FILLED }, topDownItemItemId) then -- Bath tube + return false + elseif ItemType(topDownItemItemId):isPodium() then -- Podium + self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end end end @@ -315,7 +321,8 @@ function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, end -- Reward System - if toPosition.x == CONTAINER_POSITION then + local containerThing = tile and tile:getItemByType(ITEM_TYPE_CONTAINER) + if containerThing and toPosition.x == CONTAINER_POSITION then local containerId = toPosition.y - 64 local container = self:getContainerById(containerId) if not container then @@ -347,19 +354,20 @@ function Player:onMoveItem(item, count, fromPosition, toPosition, fromCylinder, return false end - -- Players cannot throw items on reward chest - local tileChest = Tile(toPosition) - if tileChest and tileChest:getItemById(ITEM_REWARD_CHEST) then - self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) - self:getPosition():sendMagicEffect(CONST_ME_POFF) - return false - end + if tile then + -- Players cannot throw items on reward chest + if tile:getItemById(ITEM_REWARD_CHEST) then + self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end - if tile and tile:getItemById(370) then -- Trapdoor - self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) - self:getPosition():sendMagicEffect(CONST_ME_POFF) - return false + if tile:getItemById(370) then + self:sendCancelMessage(RETURNVALUE_NOTPOSSIBLE) + self:getPosition():sendMagicEffect(CONST_ME_POFF) + return false + end end if not antiPush(self, item, count, fromPosition, toPosition, fromCylinder, toCylinder) then diff --git a/data/items/items.xml b/data/items/items.xml index c1b8e8a63..379095e5d 100644 --- a/data/items/items.xml +++ b/data/items/items.xml @@ -74883,21 +74883,12 @@ Granted by TibiaGoals.com"/> - - - - - - - - - - - + + + - - - + + @@ -74916,6 +74907,22 @@ Granted by TibiaGoals.com"/> + + + + + + + + + + + + + + + + @@ -74947,6 +74954,190 @@ Granted by TibiaGoals.com"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -75117,6 +75308,208 @@ Granted by TibiaGoals.com"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -75850,6 +76243,9 @@ Granted by TibiaGoals.com"/> + + + diff --git a/data/libs/systems/hazard.lua b/data/libs/systems/hazard.lua index a1501b2d1..387aef874 100644 --- a/data/libs/systems/hazard.lua +++ b/data/libs/systems/hazard.lua @@ -193,16 +193,16 @@ function HazardMonster.onSpawn(monster, position) if not zones then return true end + + logger.debug("Monster {} spawned in hazard zone, position {}", monster:getName(), position:toString()) for _, zone in ipairs(zones) do local hazard = Hazard.getByName(zone:getName()) if hazard then monster:hazard(true) - if hazard then - monster:hazardCrit(hazard.crit) - monster:hazardDodge(hazard.dodge) - monster:hazardDamageBoost(hazard.damageBoost) - monster:hazardDefenseBoost(hazard.defenseBoost) - end + monster:hazardCrit(hazard.crit) + monster:hazardDodge(hazard.dodge) + monster:hazardDamageBoost(hazard.damageBoost) + monster:hazardDefenseBoost(hazard.defenseBoost) end end return true diff --git a/data/libs/systems/zones.lua b/data/libs/systems/zones.lua index 698a464fe..a232a071f 100644 --- a/data/libs/systems/zones.lua +++ b/data/libs/systems/zones.lua @@ -15,12 +15,24 @@ function Zone:randomPosition() logger.error("Zone:randomPosition() - Zone {} has no positions", self:getName()) return nil end - local destination = positions[math.random(1, #positions)] - local tile = destination:getTile() - while not tile or not tile:isWalkable(false, false, false, false, true) do - destination = positions[math.random(1, #positions)] - tile = destination:getTile() + + local validPositions = {} + for _, position in ipairs(positions) do + local tile = position:getTile() + if tile and tile:isWalkable(false, false, false, false, true) then + table.insert(validPositions, position) + else + logger.debug("Zone:randomPosition() - Position {} is invalid (Tile: {}, Walkable: {})", position, tile or "nil", tile and tile:isWalkable(false, false, false, false, true) or "false") + end end + + if #validPositions == 0 then + logger.error("Zone:randomPosition() - No valid positions in Zone {}", self:getName()) + return nil + end + + local destination = validPositions[math.random(1, #validPositions)] + logger.debug("Zone:randomPosition() - Selected valid position: {}", destination) return destination end diff --git a/data/libs/tables/doors.lua b/data/libs/tables/doors.lua index c87e8cb01..3daafd890 100644 --- a/data/libs/tables/doors.lua +++ b/data/libs/tables/doors.lua @@ -225,6 +225,7 @@ QuestDoorTable = { { closedDoor = 36547, openDoor = 36548 }, { closedDoor = 39351, openDoor = 39353 }, { closedDoor = 39352, openDoor = 39354 }, + { closedDoor = 42744, openDoor = 42745 }, } -- Level doors. diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua index 433abf344..abd125e79 100644 --- a/data/modules/scripts/gamestore/init.lua +++ b/data/modules/scripts/gamestore/init.lua @@ -1487,67 +1487,74 @@ GameStore.canChangeToName = function(name) local result = { ability = false, } - if name:len() < 3 or name:len() > 18 then - result.reason = "The length of your new name must be between 3 and 18 characters." + + if name:len() < 3 or name:len() > 29 then + result.reason = "The length of your new name must be between 3 and 29 characters." return result end local match = name:gmatch("%s+") local count = 0 - for v in match do + for _ in match do count = count + 1 end local matchtwo = name:match("^%s+") if matchtwo then - result.reason = "Your new name can't have whitespace at begin." + result.reason = "Your new name can't have whitespace at the beginning." return result end - if count > 1 then - result.reason = "Your new name have more than 1 whitespace." + if count > 2 then + result.reason = "Your new name can't have more than 2 spaces." + return result + end + + if name:match("%s%s") then + result.reason = "Your new name can't have consecutive spaces." return result end -- just copied from znote aac. local words = { "owner", "gamemaster", "hoster", "admin", "staff", "tibia", "account", "god", "anal", "ass", "fuck", "sex", "hitler", "pussy", "dick", "rape", "adm", "cm", "gm", "tutor", "counsellor" } local split = name:split(" ") - for k, word in ipairs(words) do - for k, nameWord in ipairs(split) do + for _, word in ipairs(words) do + for _, nameWord in ipairs(split) do if nameWord:lower() == word then - result.reason = "You can't use word \"" .. word .. '" in your new name.' + result.reason = "You can't use the word '" .. word .. "' in your new name." return result end end end local tmpName = name:gsub("%s+", "") - for i = 1, #words do - if tmpName:lower():find(words[i]) then - result.reason = "You can't use word \"" .. words[i] .. '" with whitespace in your new name.' + for _, word in ipairs(words) do + if tmpName:lower():find(word) then + result.reason = "You can't use the word '" .. word .. "' even with spaces in your new name." return result end end if MonsterType(name) then - result.reason = 'Your new name "' .. name .. "\" can't be a monster's name." + result.reason = "Your new name '" .. name .. "' can't be a monster's name." return result elseif Npc(name) then - result.reason = 'Your new name "' .. name .. "\" can't be a npc's name." + result.reason = "Your new name '" .. name .. "' can't be an NPC's name." return result end local letters = "{}|_*+-=<>0123456789@#%^&()/*'\\.,:;~!\"$" for i = 1, letters:len() do local c = letters:sub(i, i) - for i = 1, name:len() do - local m = name:sub(i, i) + for j = 1, name:len() do + local m = name:sub(j, j) if m == c then - result.reason = "You can't use this letter \"" .. c .. '" in your new name.' + result.reason = "You can't use this character '" .. c .. "' in your new name." return result end end end + result.ability = true return result end diff --git a/data/npclib/npc.lua b/data/npclib/npc.lua index 4de60c45c..4a8097ff2 100644 --- a/data/npclib/npc.lua +++ b/data/npclib/npc.lua @@ -89,7 +89,7 @@ function SayEvent(npcId, playerId, messageDelayed, npcHandler, textType) local parseInfo = { [TAG_PLAYERNAME] = player:getName(), [TAG_TIME] = getFormattedWorldTime(), - [TAG_BLESSCOST] = Blessings.getBlessingsCost(player:getLevel(), false), + [TAG_BLESSCOST] = Blessings.getBlessingCost(player:getLevel(), false, (npc:getName() == "Kais" or npc:getName() == "Nomad") and true), [TAG_PVPBLESSCOST] = Blessings.getPvpBlessingCost(player:getLevel(), false), } npc:say(npcHandler:parseMessage(messageDelayed, parseInfo), textType or TALKTYPE_PRIVATE_NP, false, player, npc:getPosition()) diff --git a/data/npclib/npc_system/modules.lua b/data/npclib/npc_system/modules.lua index 971057857..1a920134e 100644 --- a/data/npclib/npc_system/modules.lua +++ b/data/npclib/npc_system/modules.lua @@ -60,7 +60,7 @@ if Modules == nil then local parseInfo = { [TAG_PLAYERNAME] = player:getName(), [TAG_TIME] = getFormattedWorldTime(), - [TAG_BLESSCOST] = Blessings.getBlessingsCost(player:getLevel(), false), + [TAG_BLESSCOST] = Blessings.getBlessingCost(player:getLevel(), false, (npc:getName() == "Kais" or npc:getName() == "Nomad") and true), [TAG_PVPBLESSCOST] = Blessings.getPvpBlessingCost(player:getLevel(), false), [TAG_TRAVELCOST] = costMessage, } @@ -160,7 +160,7 @@ if Modules == nil then end local parseInfo = { - [TAG_BLESSCOST] = Blessings.getBlessingsCost(player:getLevel(), false), + [TAG_BLESSCOST] = Blessings.getBlessingCost(player:getLevel(), false, (npc:getName() == "Kais" or npc:getName() == "Nomad") and true), [TAG_PVPBLESSCOST] = Blessings.getPvpBlessingCost(player:getLevel(), false), } if player:hasBlessing(parameters.bless) then @@ -173,7 +173,7 @@ if Modules == nil then npc, player ) - elseif not player:removeMoneyBank(type(parameters.cost) == "string" and npcHandler:parseMessage(parameters.cost, parseInfo) or parameters.cost) then + elseif not player:removeMoneyBank(type(parameters.cost) == "string" and tonumber(npcHandler:parseMessage(parameters.cost, parseInfo)) or parameters.cost) then npcHandler:say("Oh. You do not have enough money.", npc, player) end diff --git a/data/scripts/actions/items/bed_modification_kits.lua b/data/scripts/actions/items/bed_modification_kits.lua index a406f7fd5..a50a7a683 100644 --- a/data/scripts/actions/items/bed_modification_kits.lua +++ b/data/scripts/actions/items/bed_modification_kits.lua @@ -19,8 +19,12 @@ end local bedModificationKits = Action() function bedModificationKits.onUse(player, item, fromPosition, target, toPosition, isHotkey) + if not target or type(target) ~= "userdata" or not target:isItem() then + return false + end + local newBed = setting[item:getId()] - if not newBed or not target or not target:isItem() then + if not newBed then return false end diff --git a/data/scripts/actions/items/exercise_training_weapons.lua b/data/scripts/actions/items/exercise_training_weapons.lua index 3c62d7c11..52730ad6e 100644 --- a/data/scripts/actions/items/exercise_training_weapons.lua +++ b/data/scripts/actions/items/exercise_training_weapons.lua @@ -133,7 +133,7 @@ end local exerciseTraining = Action() function exerciseTraining.onUse(player, item, fromPosition, target, toPosition, isHotkey) - if not target or type(target) == "table" or not target:getId() then + if not target or type(target) ~= "userdata" or not target:isItem() then return true end diff --git a/data/scripts/creaturescripts/player/adventure_blessing_login.lua b/data/scripts/creaturescripts/player/adventure_blessing.lua similarity index 100% rename from data/scripts/creaturescripts/player/adventure_blessing_login.lua rename to data/scripts/creaturescripts/player/adventure_blessing.lua diff --git a/data/scripts/creaturescripts/player/death.lua b/data/scripts/creaturescripts/player/death.lua new file mode 100644 index 000000000..4c0c9a8b2 --- /dev/null +++ b/data/scripts/creaturescripts/player/death.lua @@ -0,0 +1,187 @@ +local deathListEnabled = true + +local function getKillerInfo(killer) + local byPlayer = 0 + local killerName + + if killer then + if killer:isPlayer() then + byPlayer = 1 + else + local master = killer:getMaster() + if master and master ~= killer and master:isPlayer() then + killer = master + byPlayer = 1 + end + end + + killerName = killer:isMonster() and killer:getType():getNameDescription() or killer:getName() + else + killerName = "field item" + end + + return killerName, byPlayer +end + +local function getMostDamageInfo(mostDamageKiller) + local byPlayerMostDamage = 0 + local mostDamageKillerName + + if mostDamageKiller then + if mostDamageKiller:isPlayer() then + byPlayerMostDamage = 1 + else + local master = mostDamageKiller:getMaster() + if master and master ~= mostDamageKiller and master:isPlayer() then + mostDamageKiller = master + byPlayerMostDamage = 1 + end + end + + mostDamageKillerName = mostDamageKiller:isMonster() and mostDamageKiller:getType():getNameDescription() or mostDamageKiller:getName() + else + mostDamageKillerName = "field item" + end + + return mostDamageKillerName, byPlayerMostDamage +end + +local function saveDeathRecord(playerGuid, player, killerName, byPlayer, mostDamageName, byPlayerMostDamage, unjustified, mostDamageUnjustified) + local query = string.format( + "INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) " .. "VALUES (%d, %d, %d, %s, %d, %s, %d, %d, %d)", + playerGuid, + os.time(), + player:getLevel(), + db.escapeString(killerName), + byPlayer, + db.escapeString(mostDamageName), + byPlayerMostDamage, + unjustified and 1 or 0, + mostDamageUnjustified and 1 or 0 + ) + db.query(query) +end + +local function getDeathRecords(playerGuid) + local resultId = db.storeQuery("SELECT `player_id` FROM `player_deaths` WHERE `player_id` = " .. playerGuid) + local deathRecords = 0 + while resultId do + resultId = Result.next(resultId) + deathRecords = deathRecords + 1 + end + + if resultId then + Result.free(resultId) + end + + return deathRecords +end + +local function handleGuildWar(player, killer, mostDamageKiller, killerName, mostDamageName) + if not player or not killer or not killer:isPlayer() or not player:getGuild() or not killer:getGuild() then + return + end + + local playerGuildId = player:getGuild():getId() + local killerGuildId = killer:getGuild():getId() + + if playerGuildId == killerGuildId then + return + end + + if getDeathRecords(player:getGuid()) > 0 then + local warId = checkForGuildWar(playerGuildId, killerGuildId) + if warId then + recordGuildWarKill(killer, player, killerGuildId, playerGuildId, warId) + checkAndUpdateGuildWarScore(warId, playerGuildId, killerGuildId, player:getName(), killerName, mostDamageName) + end + end +end + +local function checkForGuildWar(targetGuildId, killerGuildId) + local resultId = db.storeQuery(string.format("SELECT `id` FROM `guild_wars` WHERE `status` = 1 AND ((`guild1` = %d AND `guild2` = %d) OR (`guild1` = %d AND `guild2` = %d))", killerGuildId, targetGuildId, targetGuildId, killerGuildId)) + + local warId = false + if resultId then + warId = Result.getNumber(resultId, "id") + Result.free(resultId) + end + + return warId +end + +local function recordGuildWarKill(killer, player, killerGuildId, targetGuildId, warId) + local playerName = player:getName() + db.asyncQuery(string.format("INSERT INTO `guildwar_kills` (`killer`, `target`, `killerguild`, `targetguild`, `time`, `warid`) VALUES ('%s', '%s', %d, %d, %d, %d)", db.escapeString(killer:getName()), db.escapeString(playerName), killerGuildId, targetGuildId, os.time(), warId)) +end + +local function checkAndUpdateGuildWarScore(warId, targetGuildId, killerGuildId, playerName, killerName, mostDamageName) + local resultId = db.storeQuery( + string.format( + "SELECT `guild_wars`.`id`, `guild_wars`.`frags_limit`, " + .. "(SELECT COUNT(1) FROM `guildwar_kills` WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild1`) AS guild1_kills, " + .. "(SELECT COUNT(1) FROM `guildwar_kills` WHERE `guildwar_kills`.`warid` = `guild_wars`.`id` AND `guildwar_kills`.`killerguild` = `guild_wars`.`guild2`) AS guild2_kills " + .. "FROM `guild_wars` WHERE (`guild1` = %d OR `guild2` = %d) AND `status` = 1 AND `id` = %d", + killerGuildId, + targetGuildId, + warId + ) + ) + + if resultId then + local guild1Kills = Result.getNumber(resultId, "guild1_kills") + local guild2Kills = Result.getNumber(resultId, "guild2_kills") + local fragsLimit = Result.getNumber(resultId, "frags_limit") + Result.free(resultId) + + local killerGuild = killer:getGuild() + local targetGuild = player:getGuild() + + updateGuildWarScore(killerGuild, targetGuild, playerName, killerName, guild1Kills, guild2Kills, fragsLimit) + endGuildWarIfLimitReached(guild1Kills, guild2Kills, fragsLimit, warId, killerGuild, targetGuild) + end +end + +local function updateGuildWarScore(killerGuild, targetGuild, playerName, killerName, guild1Kills, guild2Kills, fragsLimit) + local members = killerGuild:getMembersOnline() + for _, member in ipairs(members) do + member:sendChannelMessage(member, string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1Kills, guild2Kills, killerGuild:getName(), fragsLimit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) + end + + local enemyMembers = targetGuild:getMembersOnline() + for _, enemy in ipairs(enemyMembers) do + enemy:sendChannelMessage(enemy, string.format("%s was killed by %s. The new score is: %s %d:%d %s (frags limit: %d)", playerName, killerName, targetGuild:getName(), guild1Kills, guild2Kills, killerGuild:getName(), fragsLimit), TALKTYPE_CHANNEL_R1, CHANNEL_GUILD) + end +end + +local function endGuildWarIfLimitReached(guild1Kills, guild2Kills, fragsLimit, warId, killerGuild, targetGuild) + if guild1Kills >= fragsLimit or guild2Kills >= fragsLimit then + db.query(string.format("UPDATE `guild_wars` SET `status` = 4, `ended` = %d WHERE `status` = 1 AND `id` = %d", os.time(), warId)) + Game.broadcastMessage(string.format("%s has just won the war against %s.", killerGuild:getName(), targetGuild:getName())) + end +end + +local playerDeath = CreatureEvent("PlayerDeath") + +function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustified, mostDamageUnjustified) + if not deathListEnabled then + return + end + + local killerName, byPlayer = getKillerInfo(killer) + local mostDamageName, byPlayerMostDamage = getMostDamageInfo(mostDamageKiller) + + player:takeScreenshot(byPlayer and SCREENSHOT_TYPE_DEATHPVP or SCREENSHOT_TYPE_DEATHPVE) + + if mostDamageKiller and mostDamageKiller:isPlayer() then + mostDamageKiller:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL) + end + + local playerGuid = player:getGuid() + saveDeathRecord(playerGuid, player, killerName, byPlayer, mostDamageName, byPlayerMostDamage, unjustified, mostDamageUnjustified) + + Webhook.sendMessage(":skull_crossbones: " .. player:getMarkdownLink() .. " has died. Killed at level _" .. player:getLevel() .. "_ by **" .. killerName .. "**.", announcementChannels["player-kills"]) + handleGuildWar(player, killer, mostDamageKiller, killerName, mostDamageName) +end + +playerDeath:register() diff --git a/data/scripts/globalevents/server_initialization.lua b/data/scripts/globalevents/server_initialization.lua index df29660d3..a58cf01d3 100644 --- a/data/scripts/globalevents/server_initialization.lua +++ b/data/scripts/globalevents/server_initialization.lua @@ -27,29 +27,6 @@ local function moveExpiredBansToHistory() end end --- Function to check and process house auctions -local function processHouseAuctions() - local resultId = db.storeQuery("SELECT `id`, `highest_bidder`, `last_bid`, " .. "(SELECT `balance` FROM `players` WHERE `players`.`id` = `highest_bidder`) AS `balance` " .. "FROM `houses` WHERE `owner` = 0 AND `bid_end` != 0 AND `bid_end` < " .. os.time()) - if resultId then - repeat - local house = House(Result.getNumber(resultId, "id")) - if house then - local highestBidder = Result.getNumber(resultId, "highest_bidder") - local balance = Result.getNumber(resultId, "balance") - local lastBid = Result.getNumber(resultId, "last_bid") - if balance >= lastBid then - db.query("UPDATE `players` SET `balance` = " .. (balance - lastBid) .. " WHERE `id` = " .. highestBidder) - house:setHouseOwner(highestBidder) - end - - db.asyncQuery("UPDATE `houses` SET `last_bid` = 0, `bid_end` = 0, `highest_bidder` = 0, `bid` = 0 " .. "WHERE `id` = " .. house:getId()) - end - until not Result.next(resultId) - - Result.free(resultId) - end -end - -- Function to store towns in the database local function storeTownsInDatabase() db.query("TRUNCATE TABLE `towns`") @@ -150,7 +127,6 @@ function serverInitialization.onStartup() cleanupDatabase() moveExpiredBansToHistory() - processHouseAuctions() storeTownsInDatabase() checkAndLogDuplicateValues({ "Global", "GlobalStorage", "Storage" }) updateEventRates() diff --git a/data/scripts/lib/register_monster_type.lua b/data/scripts/lib/register_monster_type.lua index ed5de6421..81960f4f8 100644 --- a/data/scripts/lib/register_monster_type.lua +++ b/data/scripts/lib/register_monster_type.lua @@ -17,6 +17,10 @@ end registerMonsterType.name = function(mtype, mask) if mask.name then mtype:name(mask.name) + -- Try register hazard monsters + mtype.onSpawn = function(monster, spawnPosition) + HazardMonster.onSpawn(monster, spawnPosition) + end end end registerMonsterType.description = function(mtype, mask) @@ -194,7 +198,7 @@ registerMonsterType.flags = function(mtype, mask) end if mask.flags.rewardBoss then mtype:isRewardBoss(mask.flags.rewardBoss) - mtype.onSpawn = function(monster) + mtype.onSpawn = function(monster, spawnPosition) monster:setRewardBoss() end end diff --git a/data/scripts/lib/register_spells.lua b/data/scripts/lib/register_spells.lua index 8c5498596..f2e7cacee 100644 --- a/data/scripts/lib/register_spells.lua +++ b/data/scripts/lib/register_spells.lua @@ -386,7 +386,7 @@ AREA_RING1_BURST3 = { { 0, 0, 1, 1, 1, 1, 1, 0, 0 }, { 0, 1, 1, 1, 1, 1, 1, 1, 0 }, { 1, 1, 1, 0, 0, 0, 1, 1, 1 }, - { 1, 1, 1, 0, 3, 0, 1, 1, 1 }, + { 1, 1, 1, 0, 2, 0, 1, 1, 1 }, { 1, 1, 1, 0, 0, 0, 1, 1, 1 }, { 0, 1, 1, 1, 1, 1, 1, 1, 0 }, { 0, 0, 1, 1, 1, 1, 1, 0, 0 }, diff --git a/data/scripts/systems/item_tiers.lua b/data/scripts/systems/item_tiers.lua index 06a8321a0..cc7a2a269 100644 --- a/data/scripts/systems/item_tiers.lua +++ b/data/scripts/systems/item_tiers.lua @@ -90,7 +90,7 @@ local itemTierClassifications = { }, } --- Item tier with gold price for uprading it +-- Item tier with gold price for upgrading it for classificationId, classificationTable in ipairs(itemTierClassifications) do local itemClassification = Game.createItemClassification(classificationId) local classification = {} diff --git a/data/scripts/talkactions/player/buy_house.lua b/data/scripts/talkactions/player/buy_house.lua index c3784d81a..84d3a34aa 100644 --- a/data/scripts/talkactions/player/buy_house.lua +++ b/data/scripts/talkactions/player/buy_house.lua @@ -60,6 +60,8 @@ function buyHouse.onSay(player, words, param) return true end -buyHouse:separator(" ") -buyHouse:groupType("normal") -buyHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + buyHouse:separator(" ") + buyHouse:groupType("normal") + buyHouse:register() +end diff --git a/data/scripts/talkactions/player/leave_house.lua b/data/scripts/talkactions/player/leave_house.lua index 20ad186f2..d954eb1dc 100644 --- a/data/scripts/talkactions/player/leave_house.lua +++ b/data/scripts/talkactions/player/leave_house.lua @@ -42,6 +42,8 @@ function leaveHouse.onSay(player, words, param) return true end -leaveHouse:separator(" ") -leaveHouse:groupType("normal") -leaveHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + leaveHouse:separator(" ") + leaveHouse:groupType("normal") + leaveHouse:register() +end diff --git a/data/scripts/talkactions/player/sell_house.lua b/data/scripts/talkactions/player/sell_house.lua index c96cb5f71..dadadd066 100644 --- a/data/scripts/talkactions/player/sell_house.lua +++ b/data/scripts/talkactions/player/sell_house.lua @@ -20,6 +20,8 @@ function sellHouse.onSay(player, words, param) return true end -sellHouse:separator(" ") -sellHouse:groupType("normal") -sellHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + sellHouse:separator(" ") + sellHouse:groupType("normal") + sellHouse:register() +end diff --git a/schema.sql b/schema.sql index 86ea9e1bf..6fe1f21cb 100644 --- a/schema.sql +++ b/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `server_config` ( CONSTRAINT `server_config_pk` PRIMARY KEY (`config`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '46'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '48'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); -- Table structure `accounts` CREATE TABLE IF NOT EXISTS `accounts` ( @@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `accounts` ( `tournament_coins` int(12) UNSIGNED NOT NULL DEFAULT '0', `creation` int(11) UNSIGNED NOT NULL DEFAULT '0', `recruiter` INT(6) DEFAULT 0, + `house_bid_id` int(11) NOT NULL DEFAULT '0', CONSTRAINT `accounts_pk` PRIMARY KEY (`id`), CONSTRAINT `accounts_unique` UNIQUE (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -451,13 +452,16 @@ CREATE TABLE IF NOT EXISTS `houses` ( `name` varchar(255) NOT NULL, `rent` int(11) NOT NULL DEFAULT '0', `town_id` int(11) NOT NULL DEFAULT '0', - `bid` int(11) NOT NULL DEFAULT '0', - `bid_end` int(11) NOT NULL DEFAULT '0', - `last_bid` int(11) NOT NULL DEFAULT '0', - `highest_bidder` int(11) NOT NULL DEFAULT '0', `size` int(11) NOT NULL DEFAULT '0', `guildid` int(11), `beds` int(11) NOT NULL DEFAULT '0', + `bidder` int(11) NOT NULL DEFAULT '0', + `bidder_name` varchar(255) NOT NULL DEFAULT '', + `highest_bid` int(11) NOT NULL DEFAULT '0', + `internal_bid` int(11) NOT NULL DEFAULT '0', + `bid_end_date` int(11) NOT NULL DEFAULT '0', + `state` smallint(5) UNSIGNED NOT NULL DEFAULT '0', + `transfer_status` tinyint(1) DEFAULT '0', INDEX `owner` (`owner`), INDEX `town_id` (`town_id`), CONSTRAINT `houses_pk` PRIMARY KEY (`id`) diff --git a/src/account/account.cpp b/src/account/account.cpp index 93596f77b..db79f9425 100644 --- a/src/account/account.cpp +++ b/src/account/account.cpp @@ -300,3 +300,10 @@ uint32_t Account::getAccountAgeInDays() const { [[nodiscard]] time_t Account::getPremiumLastDay() const { return m_account->premiumLastDay; } + +uint32_t Account::getHouseBidId() const { + return m_account->houseBidId; +} +void Account::setHouseBidId(uint32_t houseId) { + m_account->houseBidId = houseId; +} diff --git a/src/account/account.hpp b/src/account/account.hpp index 2c6098a8d..0a2bcc1a2 100644 --- a/src/account/account.hpp +++ b/src/account/account.hpp @@ -119,6 +119,9 @@ class Account { std::tuple, AccountErrors_t> getAccountPlayers() const; + void setHouseBidId(uint32_t houseId); + uint32_t getHouseBidId() const; + // Old protocol compat void setProtocolCompat(bool toggle); diff --git a/src/account/account_info.hpp b/src/account/account_info.hpp index b9dad60db..54741419d 100644 --- a/src/account/account_info.hpp +++ b/src/account/account_info.hpp @@ -28,4 +28,5 @@ struct AccountInfo { time_t sessionExpires = 0; uint32_t premiumDaysPurchased = 0; uint32_t creationTime = 0; + uint32_t houseBidId = 0; }; diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index c3f02bfe9..b2e8fd807 100644 --- a/src/account/account_repository_db.cpp +++ b/src/account/account_repository_db.cpp @@ -47,12 +47,13 @@ bool AccountRepositoryDB::loadBySession(const std::string &sessionKey, std::uniq bool AccountRepositoryDB::save(const std::unique_ptr &accInfo) { bool successful = g_database().executeQuery( fmt::format( - "UPDATE `accounts` SET `type` = {}, `premdays` = {}, `lastday` = {}, `creation` = {}, `premdays_purchased` = {} WHERE `id` = {}", + "UPDATE `accounts` SET `type` = {}, `premdays` = {}, `lastday` = {}, `creation` = {}, `premdays_purchased` = {}, `house_bid_id` = {} WHERE `id` = {}", accInfo->accountType, accInfo->premiumRemainingDays, accInfo->premiumLastDay, accInfo->creationTime, accInfo->premiumDaysPurchased, + accInfo->houseBidId, accInfo->id ) ); diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 559045fdb..65e585159 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -46,6 +46,7 @@ enum ConfigKey_t : uint16_t { CONVERT_UNSAFE_SCRIPTS, CORE_DIRECTORY, CRITICALCHANCE, + CYCLOPEDIA_HOUSE_AUCTION, DATA_DIRECTORY, DAY_KILLS_TO_RED, DEATH_LOSE_PERCENT, @@ -110,6 +111,7 @@ enum ConfigKey_t : uint16_t { HAZARD_PODS_TIME_TO_DAMAGE, HAZARD_PODS_TIME_TO_SPAWN, HAZARD_SPAWN_PLUNDER_MULTIPLIER, + DAYS_TO_CLOSE_BID, HOUSE_BUY_LEVEL, HOUSE_LOSE_AFTER_INACTIVITY, HOUSE_OWNED_BY_ACCOUNT, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1c00df76c..1034a28be 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -157,6 +157,7 @@ bool ConfigManager::load() { loadBoolConfig(L, VIP_SYSTEM_ENABLED, "vipSystemEnabled", false); loadBoolConfig(L, WARN_UNSAFE_SCRIPTS, "warnUnsafeScripts", true); loadBoolConfig(L, XP_DISPLAY_MODE, "experienceDisplayRates", true); + loadBoolConfig(L, CYCLOPEDIA_HOUSE_AUCTION, "toggleCyclopediaHouseAuction", true); loadFloatConfig(L, BESTIARY_RATE_CHARM_SHOP_PRICE, "bestiaryRateCharmShopPrice", 1.0); loadFloatConfig(L, COMBAT_CHAIN_SKILL_FORMULA_AXE, "combatChainSkillFormulaAxe", 0.9); @@ -255,6 +256,7 @@ bool ConfigManager::load() { loadIntConfig(L, HAZARD_PODS_TIME_TO_DAMAGE, "hazardPodsTimeToDamage", 2000); loadIntConfig(L, HAZARD_PODS_TIME_TO_SPAWN, "hazardPodsTimeToSpawn", 4000); loadIntConfig(L, HAZARD_SPAWN_PLUNDER_MULTIPLIER, "hazardSpawnPlunderMultiplier", 25); + loadIntConfig(L, DAYS_TO_CLOSE_BID, "daysToCloseBid", 7); loadIntConfig(L, HOUSE_BUY_LEVEL, "houseBuyLevel", 0); loadIntConfig(L, HOUSE_LOSE_AFTER_INACTIVITY, "houseLoseAfterInactivity", 0); loadIntConfig(L, HOUSE_PRICE_PER_SQM, "housePriceEachSQM", 1000); diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 32f3c598b..4d02f54c3 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -140,6 +140,20 @@ void Creature::onThink(uint32_t interval) { onThink(); } +void Creature::checkCreatureAttack(bool now) { + if (now) { + if (isAlive()) { + onAttacking(0); + } + return; + } + + g_dispatcher().addEvent([self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + creature->checkCreatureAttack(true); + } }, "Creature::checkCreatureAttack"); +} + void Creature::onAttacking(uint32_t interval) { const auto &attackedCreature = getAttackedCreature(); if (!attackedCreature) { @@ -162,7 +176,7 @@ void Creature::onIdleStatus() { } void Creature::onCreatureWalk() { - if (checkingWalkCreature) { + if (checkingWalkCreature || isRemoved() || isDead()) { return; } @@ -170,7 +184,11 @@ void Creature::onCreatureWalk() { metrics::method_latency measure(__METRICS_METHOD_NAME__); - g_dispatcher().addWalkEvent([self = getCreature(), this] { + g_dispatcher().addWalkEvent([self = std::weak_ptr(getCreature()), this] { + if (!self.lock()) { + return; + } + checkingWalkCreature = false; if (isRemoved()) { return; @@ -269,12 +287,16 @@ void Creature::addEventWalk(bool firstStep) { safeCall([this, ticks]() { // Take first step right away, but still queue the next if (ticks == 1) { - g_game().checkCreatureWalk(getID()); + onCreatureWalk(); } eventWalk = g_dispatcher().scheduleEvent( - static_cast(ticks), - [creatureId = getID()] { g_game().checkCreatureWalk(creatureId); }, "Game::checkCreatureWalk" + static_cast(ticks), [self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + creature->onCreatureWalk(); + } + }, + "Game::checkCreatureWalk" ); }); } @@ -421,7 +443,7 @@ void Creature::onCreatureMove(const std::shared_ptr &creature, const s if (followCreature && (creature.get() == this || creature == followCreature)) { if (hasFollowPath) { isUpdatingPath = true; - g_game().updateCreatureWalk(getID()); // internally uses addEventWalk. + updateCreatureWalk(); } if (newPos.z != oldPos.z || !canSee(followCreature->getPosition())) { @@ -436,7 +458,7 @@ void Creature::onCreatureMove(const std::shared_ptr &creature, const s } else { if (hasExtraSwing()) { // our target is moving lets see if we can get in hit - g_dispatcher().addEvent([creatureId = getID()] { g_game().checkCreatureAttack(creatureId); }, "Game::checkCreatureAttack"); + checkCreatureAttack(); } if (newTile->getZoneType() != oldTile->getZoneType()) { @@ -689,6 +711,10 @@ std::shared_ptr Creature::getCorpse(const std::shared_ptr &, con } void Creature::changeHealth(int32_t healthChange, bool sendHealthChange /* = true*/) { + if (isLifeless()) { + return; + } + int32_t oldHealth = health; if (healthChange > 0) { @@ -701,7 +727,13 @@ void Creature::changeHealth(int32_t healthChange, bool sendHealthChange /* = tru g_game().addCreatureHealth(static_self_cast()); } if (health <= 0) { - g_dispatcher().addEvent([creatureId = getID()] { g_game().executeDeath(creatureId); }, "Game::executeDeath"); + g_dispatcher().addEvent([self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + if (!creature->isRemoved()) { + g_game().afterCreatureZoneChange(creature, creature->getZones(), {}); + creature->onDeath(); + } + } }, "Game::executeDeath"); } } @@ -874,6 +906,10 @@ void Creature::getPathSearchParams(const std::shared_ptr &, FindPathPa } void Creature::goToFollowCreature_async(std::function &&onComplete) { + if (isDead()) { + return; + } + if (!hasAsyncTaskFlag(Pathfinder) && onComplete) { g_dispatcher().addEvent(std::move(onComplete), "goToFollowCreature_async"); } @@ -1781,7 +1817,7 @@ void Creature::sendAsyncTasks() { setAsyncTaskFlag(AsyncTaskRunning, true); g_dispatcher().asyncEvent([self = std::weak_ptr(getCreature())] { if (const auto &creature = self.lock()) { - if (!creature->isRemoved()) { + if (!creature->isRemoved() && creature->isAlive()) { for (const auto &task : creature->asyncTasks) { task(); } diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index fcea0a6b0..bea95313f 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -209,7 +209,11 @@ class Creature : virtual public Thing, public SharedObject { } bool isAlive() const { - return !isDead(); + return !isLifeless(); + } + + bool isLifeless() const { + return health <= 0; } virtual int32_t getMaxHealth() const { @@ -312,6 +316,9 @@ class Creature : virtual public Thing, public SharedObject { void addEventWalk(bool firstStep = false); void stopEventWalk(); + void updateCreatureWalk() { + goToFollowCreature_async(); + } void goToFollowCreature_async(std::function &&onComplete = nullptr); virtual void goToFollowCreature(); @@ -482,6 +489,9 @@ class Creature : virtual public Thing, public SharedObject { void setCreatureLight(LightInfo lightInfo); virtual void onThink(uint32_t interval); + + void checkCreatureAttack(bool now = false); + void onAttacking(uint32_t interval); virtual void onCreatureWalk(); virtual bool getNextStep(Direction &dir, uint32_t &flags); @@ -694,7 +704,8 @@ class Creature : virtual public Thing, public SharedObject { AsyncTaskRunning = 1 << 0, UpdateTargetList = 1 << 1, UpdateIdleStatus = 1 << 2, - Pathfinder = 1 << 3 + Pathfinder = 1 << 3, + OnThink = 1 << 4, }; virtual bool isDead() const { diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index 7276bea19..13b30321a 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -354,7 +354,9 @@ void Monster::onRemoveCreature(const std::shared_ptr &creature, bool i setIdle(true); } else { - onCreatureLeave(creature); + addAsyncTask([this, creature] { + onCreatureLeave(creature); + }); } } @@ -421,27 +423,13 @@ void Monster::onCreatureMove(const std::shared_ptr &creature, const st if (const auto &nextTile = g_game().map.getTile(checkPosition)) { const auto &topCreature = nextTile->getTopCreature(); if (followCreature != topCreature && isOpponent(topCreature)) { - g_dispatcher().addEvent([selfWeak = std::weak_ptr(getMonster()), topCreatureWeak = std::weak_ptr(topCreature)] { - const auto &self = selfWeak.lock(); - const auto &topCreature = topCreatureWeak.lock(); - if (self && topCreature) { - self->selectTarget(topCreature); - } - }, - "Monster::onCreatureMove"); + selectTarget(topCreature); } } } } else if (isOpponent(creature)) { // we have no target lets try pick this one - g_dispatcher().addEvent([selfWeak = std::weak_ptr(getMonster()), creatureWeak = std::weak_ptr(creature)] { - const auto &self = selfWeak.lock(); - const auto &creaturePtr = creatureWeak.lock(); - if (self && creaturePtr) { - self->selectTarget(creaturePtr); - } - }, - "Monster::onCreatureMove"); + selectTarget(creature); } } }; @@ -513,9 +501,9 @@ void Monster::onAttackedByPlayer(const std::shared_ptr &attackerPlayer) } } -void Monster::onSpawn() { +void Monster::onSpawn(const Position &position) { if (mType->info.spawnEvent != -1) { - // onSpawn(self) + // onSpawn(self, spawnPosition) LuaScriptInterface* scriptInterface = mType->info.scriptInterface; if (!scriptInterface->reserveScriptEnv()) { g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua " @@ -532,8 +520,9 @@ void Monster::onSpawn() { LuaScriptInterface::pushUserdata(L, getMonster()); LuaScriptInterface::setMetatable(L, -1, "Monster"); + LuaScriptInterface::pushPosition(L, position); - scriptInterface->callVoidFunction(1); + scriptInterface->callVoidFunction(2); } } @@ -602,7 +591,7 @@ bool Monster::removeTarget(const std::shared_ptr &creature) { } void Monster::updateTargetList() { - if (g_dispatcher().context().getGroup() == TaskGroup::Walk) { + if (!g_dispatcher().context().isAsync()) { setAsyncTaskFlag(UpdateTargetList, true); return; } @@ -952,7 +941,7 @@ bool Monster::selectTarget(const std::shared_ptr &creature) { if (isHostile() || isSummon()) { if (setAttackedCreature(creature)) { - g_dispatcher().addEvent([creatureId = getID()] { g_game().checkCreatureAttack(creatureId); }, __FUNCTION__); + checkCreatureAttack(); } } return setFollowCreature(creature); @@ -976,7 +965,7 @@ void Monster::setIdle(bool idle) { } void Monster::updateIdleStatus() { - if (g_dispatcher().context().getGroup() == TaskGroup::Walk) { + if (!g_dispatcher().context().isAsync()) { setAsyncTaskFlag(UpdateIdleStatus, true); return; } @@ -1071,8 +1060,11 @@ void Monster::onThink(uint32_t interval) { } updateIdleStatus(); + setAsyncTaskFlag(OnThink, true); +} - if (isIdle) { +void Monster::onThink_async() { + if (isIdle) { // updateIdleStatus(); is executed before this method return; } @@ -1107,15 +1099,18 @@ void Monster::onThink(uint32_t interval) { } } - onThinkTarget(interval); - onThinkYell(interval); - onThinkDefense(interval); - onThinkSound(interval); + onThinkTarget(EVENT_CREATURE_THINK_INTERVAL); + + safeCall([this] { + onThinkYell(EVENT_CREATURE_THINK_INTERVAL); + onThinkDefense(EVENT_CREATURE_THINK_INTERVAL); + onThinkSound(EVENT_CREATURE_THINK_INTERVAL); + }); } void Monster::doAttacking(uint32_t interval) { const auto &attackedCreature = getAttackedCreature(); - if (!attackedCreature || (isSummon() && attackedCreature.get() == this)) { + if (!attackedCreature || attackedCreature->isLifeless() || (isSummon() && attackedCreature.get() == this)) { return; } @@ -1587,73 +1582,44 @@ bool Monster::getDanceStep(const Position &creaturePos, Direction &moveDirection uint32_t centerToDist = std::max(distance_x, distance_y); // monsters not at targetDistance shouldn't dancestep - if (centerToDist < (uint32_t)targetDistance) { + if (centerToDist < static_cast(targetDistance)) { return false; } std::vector dirList; - if (!keepDistance || offset_y >= 0) { - uint32_t tmpDist = std::max(distance_x, std::abs((creaturePos.getY() - 1) - centerPos.getY())); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_NORTH)) { + auto tryAddDirection = [&](Direction direction, int_fast32_t newX, int_fast32_t newY) { + uint32_t tmpDist = std::max(std::abs(newX - centerPos.getX()), std::abs(newY - centerPos.getY())); + if (tmpDist == centerToDist && canWalkTo(creaturePos, direction)) { bool result = true; if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x, creaturePos.y - 1, creaturePos.z), attackedCreature)); + result = (!canDoAttackNow || canUseAttack(Position(newX, newY, creaturePos.z), attackedCreature)); } if (result) { - dirList.push_back(DIRECTION_NORTH); + dirList.emplace_back(direction); } } + }; + + if (!keepDistance || offset_y >= 0) { + tryAddDirection(DIRECTION_NORTH, creaturePos.getX(), creaturePos.getY() - 1); } if (!keepDistance || offset_y <= 0) { - uint32_t tmpDist = std::max(distance_x, std::abs((creaturePos.getY() + 1) - centerPos.getY())); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_SOUTH)) { - bool result = true; - - if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x, creaturePos.y + 1, creaturePos.z), attackedCreature)); - } - - if (result) { - dirList.push_back(DIRECTION_SOUTH); - } - } + tryAddDirection(DIRECTION_SOUTH, creaturePos.getX(), creaturePos.getY() + 1); } if (!keepDistance || offset_x <= 0) { - uint32_t tmpDist = std::max(std::abs((creaturePos.getX() + 1) - centerPos.getX()), distance_y); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_EAST)) { - bool result = true; - - if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x + 1, creaturePos.y, creaturePos.z), attackedCreature)); - } - - if (result) { - dirList.push_back(DIRECTION_EAST); - } - } + tryAddDirection(DIRECTION_EAST, creaturePos.getX() + 1, creaturePos.getY()); } if (!keepDistance || offset_x >= 0) { - uint32_t tmpDist = std::max(std::abs((creaturePos.getX() - 1) - centerPos.getX()), distance_y); - if (tmpDist == centerToDist && canWalkTo(creaturePos, DIRECTION_WEST)) { - bool result = true; - - if (keepAttack) { - result = (!canDoAttackNow || canUseAttack(Position(creaturePos.x - 1, creaturePos.y, creaturePos.z), attackedCreature)); - } - - if (result) { - dirList.push_back(DIRECTION_WEST); - } - } + tryAddDirection(DIRECTION_WEST, creaturePos.getX() - 1, creaturePos.getY()); } if (!dirList.empty()) { - std::shuffle(dirList.begin(), dirList.end(), getRandomGenerator()); + std::ranges::shuffle(dirList, getRandomGenerator()); moveDirection = dirList[uniform_random(0, dirList.size() - 1)]; return true; } @@ -2659,4 +2625,8 @@ void Monster::onExecuteAsyncTasks() { if (hasAsyncTaskFlag(UpdateIdleStatus)) { updateIdleStatus(); } + + if (hasAsyncTaskFlag(OnThink)) { + onThink_async(); + } } diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index b8a6087ac..6e44d8f36 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -91,7 +91,7 @@ class Monster final : public Creature { void onCreatureMove(const std::shared_ptr &creature, const std::shared_ptr &newTile, const Position &newPos, const std::shared_ptr &oldTile, const Position &oldPos, bool teleport) override; void onCreatureSay(const std::shared_ptr &creature, SpeakClasses type, const std::string &text) override; void onAttackedByPlayer(const std::shared_ptr &attackerPlayer); - void onSpawn(); + void onSpawn(const Position &position); void drainHealth(const std::shared_ptr &attacker, int32_t damage) override; void changeHealth(int32_t healthChange, bool sendHealthChange = true) override; @@ -225,6 +225,8 @@ class Monster final : public Creature { void onExecuteAsyncTasks() override; private: + void onThink_async(); + auto getTargetIterator(const std::shared_ptr &creature) { return std::ranges::find_if(targetList.begin(), targetList.end(), [id = creature->getID()](const std::weak_ptr &ref) { const auto &target = ref.lock(); diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp index 3a915d737..b80266f56 100644 --- a/src/creatures/monsters/spawns/spawn_monster.cpp +++ b/src/creatures/monsters/spawns/spawn_monster.cpp @@ -232,7 +232,7 @@ bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const spawnedMonsterMap[spawnMonsterId] = monster; sb.lastSpawn = OTSYS_TIME(); - monster->onSpawn(); + monster->onSpawn(sb.pos); return true; } diff --git a/src/creatures/players/grouping/party.cpp b/src/creatures/players/grouping/party.cpp index e63ca14b6..c04f8d579 100644 --- a/src/creatures/players/grouping/party.cpp +++ b/src/creatures/players/grouping/party.cpp @@ -12,6 +12,7 @@ #include "config/configmanager.hpp" #include "creatures/creature.hpp" #include "creatures/players/player.hpp" +#include "creatures/players/vocations/vocation.hpp" #include "game/game.hpp" #include "game/movement/position.hpp" #include "lua/callbacks/event_callback.hpp" @@ -61,6 +62,25 @@ size_t Party::getInvitationCount() const { return inviteList.size(); } +uint8_t Party::getUniqueVocationsCount() const { + std::unordered_set uniqueVocations; + + for (const auto &player : getPlayers()) { + if (uniqueVocations.size() >= 4) { + break; + } + + const auto &vocation = player->getVocation(); + if (!vocation) { + continue; + } + + uniqueVocations.insert(vocation->getBaseId()); + } + + return uniqueVocations.size(); +} + void Party::disband() { if (!g_events().eventPartyOnDisband(getParty())) { return; @@ -504,9 +524,11 @@ void Party::shareExperience(uint64_t experience, const std::shared_ptr g_callbacks().executeCallback(EventCallback_t::partyOnShareExperience, &EventCallback::partyOnShareExperience, getParty(), std::ref(shareExperience)); for (const auto &member : getMembers()) { - member->onGainSharedExperience(shareExperience, target); + const auto memberStaminaBoost = static_cast(member->getStaminaXpBoost()) / 100; + member->onGainSharedExperience(shareExperience * memberStaminaBoost, target); } - leader->onGainSharedExperience(shareExperience, target); + const auto leaderStaminaBoost = static_cast(leader->getStaminaXpBoost()) / 100; + leader->onGainSharedExperience(shareExperience * leaderStaminaBoost, target); } bool Party::canUseSharedExperience(const std::shared_ptr &player) { diff --git a/src/creatures/players/grouping/party.hpp b/src/creatures/players/grouping/party.hpp index cef450c0c..bc1ba77d1 100644 --- a/src/creatures/players/grouping/party.hpp +++ b/src/creatures/players/grouping/party.hpp @@ -40,6 +40,7 @@ class Party final : public SharedObject { std::vector> getInvitees(); size_t getMemberCount() const; size_t getInvitationCount() const; + uint8_t getUniqueVocationsCount() const; void disband(); bool invitePlayer(const std::shared_ptr &player); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 4335db2bd..bfbc8148d 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -37,6 +37,7 @@ #include "enums/object_category.hpp" #include "enums/player_blessings.hpp" #include "enums/player_icons.hpp" +#include "enums/player_cyclopedia.hpp" #include "game/game.hpp" #include "game/modal_window/modal_window.hpp" #include "game/scheduling/dispatcher.hpp" @@ -2264,6 +2265,22 @@ void Player::sendOutfitWindow() const { } } +void Player::sendCyclopediaHouseList(const HouseMap &houses) const { + if (client) { + client->sendCyclopediaHouseList(houses); + } +} +void Player::sendResourceBalance(Resource_t resourceType, uint64_t value) const { + if (client) { + client->sendResourceBalance(resourceType, value); + } +} +void Player::sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess /* = false*/) const { + if (client) { + client->sendHouseAuctionMessage(houseId, type, index, bidSuccess); + } +} + // Imbuements void Player::onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr &item, uint8_t slot, bool protectionCharm) { @@ -3430,9 +3447,10 @@ void Player::doAttacking(uint32_t interval) { } const auto &task = createPlayerTask( - std::max(SCHEDULER_MINTICKS, delay), - [playerId = getID()] { g_game().checkCreatureAttack(playerId); }, - __FUNCTION__ + std::max(SCHEDULER_MINTICKS, delay), [self = std::weak_ptr(getCreature())] { + if (const auto &creature = self.lock()) { + creature->checkCreatureAttack(true); + } }, __FUNCTION__ ); if (!classicSpeed) { @@ -5296,7 +5314,7 @@ bool Player::setAttackedCreature(const std::shared_ptr &creature) { } if (creature) { - g_dispatcher().addEvent([creatureId = getID()] { g_game().checkCreatureAttack(creatureId); }, __FUNCTION__); + checkCreatureAttack(); } return true; } @@ -9835,7 +9853,7 @@ void Player::onCreatureMove(const std::shared_ptr &creature, const std const auto &followCreature = getFollowCreature(); if (hasFollowPath && (creature == followCreature || (creature.get() == this && followCreature))) { isUpdatingPath = false; - g_game().updateCreatureWalk(getID()); // internally uses addEventWalk. + updateCreatureWalk(); } if (creature != getPlayer()) { @@ -10446,3 +10464,108 @@ uint16_t Player::getPlayerVocationEnum() const { return Vocation_t::VOCATION_NONE; } + +BidErrorMessage Player::canBidHouse(uint32_t houseId) { + using enum BidErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (!isPremium()) { + return Premium; + } + + if (getAccount()->getHouseBidId() != 0) { + return OnlyOneBid; + } + + if (getBankBalance() < (house->getRent() + house->getHighestBid())) { + return NotEnoughMoney; + } + + if (house->isGuildhall()) { + if (getGuildRank() && getGuildRank()->level != 3) { + return Guildhall; + } + + if (getGuild() && getGuild()->getBankBalance() < (house->getRent() + house->getHighestBid())) { + return NotEnoughGuildMoney; + } + } + + return NoError; +} + +TransferErrorMessage Player::canTransferHouse(uint32_t houseId, uint32_t newOwnerGUID) { + using enum TransferErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getGUID() != house->getOwner()) { + return NotHouseOwner; + } + + if (getGUID() == newOwnerGUID) { + return AlreadyTheOwner; + } + + const auto newOwner = g_game().getPlayerByGUID(newOwnerGUID, true); + if (!newOwner) { + return CharacterNotExist; + } + + if (newOwner->getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (!newOwner->isPremium()) { + return Premium; + } + + if (newOwner->getAccount()->getHouseBidId() != 0) { + return OnlyOneBid; + } + + return Success; +} + +AcceptTransferErrorMessage Player::canAcceptTransferHouse(uint32_t houseId) { + using enum AcceptTransferErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getGUID() != house->getBidder()) { + return NotNewOwner; + } + + if (!isPremium()) { + return Premium; + } + + if (getAccount()->getHouseBidId() != 0) { + return AlreadyBid; + } + + if (getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (getBankBalance() < (house->getRent() + house->getInternalBid())) { + return Frozen; + } + + if (house->getTransferStatus()) { + return AlreadyAccepted; + } + + return Success; +} diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 6ab3be4ca..1b52607f2 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -64,17 +64,23 @@ struct HighscoreCharacter; enum class PlayerIcon : uint8_t; enum class IconBakragore : uint8_t; +enum class HouseAuctionType : uint8_t; +enum class BidErrorMessage : uint8_t; +enum class TransferErrorMessage : uint8_t; +enum class AcceptTransferErrorMessage : uint8_t; enum ObjectCategory_t : uint8_t; enum PreySlot_t : uint8_t; enum SpeakClasses : uint8_t; enum ChannelEvent_t : uint8_t; enum SquareColor_t : uint8_t; +enum Resource_t : uint8_t; using GuildWarVector = std::vector; using StashContainerList = std::vector, uint32_t>>; using ItemVector = std::vector>; using UsersMap = std::map>; using InvitedMap = std::map>; +using HouseMap = std::map>; struct ForgeHistory { ForgeAction_t actionType = ForgeAction_t::FUSION; @@ -880,6 +886,13 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendOpenPrivateChannel(const std::string &receiver) const; void sendExperienceTracker(int64_t rawExp, int64_t finalExp) const; void sendOutfitWindow() const; + // House Auction + BidErrorMessage canBidHouse(uint32_t houseId); + TransferErrorMessage canTransferHouse(uint32_t houseId, uint32_t newOwnerGUID); + AcceptTransferErrorMessage canAcceptTransferHouse(uint32_t houseId); + void sendCyclopediaHouseList(const HouseMap &houses) const; + void sendResourceBalance(Resource_t resourceType, uint64_t value) const; + void sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess = false) const; // Imbuements void onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr &item, uint8_t slot, bool protectionCharm); void onClearImbuement(const std::shared_ptr &item, uint8_t slot); diff --git a/src/database/databasemanager.cpp b/src/database/databasemanager.cpp index 2941172f3..142a1db33 100644 --- a/src/database/databasemanager.cpp +++ b/src/database/databasemanager.cpp @@ -13,6 +13,19 @@ #include "lua/functions/core/libs/core_libs_functions.hpp" #include "lua/scripts/luascript.hpp" +namespace InternalDBManager { + int32_t extractVersionFromFilename(const std::string &filename) { + std::regex versionRegex(R"((\d+)\.lua)"); + std::smatch match; + + if (std::regex_search(filename, match, versionRegex) && match.size() > 1) { + return std::stoi(match.str(1)); + } + + return -1; + } +} + bool DatabaseManager::optimizeTables() { Database &db = Database::getInstance(); std::ostringstream query; @@ -73,48 +86,62 @@ int32_t DatabaseManager::getDatabaseVersion() { } void DatabaseManager::updateDatabase() { + Benchmark bm; lua_State* L = luaL_newstate(); if (!L) { return; } luaL_openlibs(L); - CoreLibsFunctions::init(L); - int32_t version = getDatabaseVersion(); - do { - std::ostringstream ss; - ss << g_configManager().getString(DATA_DIRECTORY) + "/migrations/" << version << ".lua"; - if (luaL_dofile(L, ss.str().c_str()) != 0) { - g_logger().error("DatabaseManager::updateDatabase - Version: {}" - "] {}", - version, lua_tostring(L, -1)); - break; - } + int32_t currentVersion = getDatabaseVersion(); + std::string migrationDirectory = g_configManager().getString(DATA_DIRECTORY) + "/migrations/"; - if (!LuaScriptInterface::reserveScriptEnv()) { - break; - } + std::vector> migrations; - lua_getglobal(L, "onUpdateDatabase"); - if (lua_pcall(L, 0, 1, 0) != 0) { - LuaScriptInterface::resetScriptEnv(); - g_logger().warn("[DatabaseManager::updateDatabase - Version: {}] {}", version, lua_tostring(L, -1)); - break; + for (const auto &entry : std::filesystem::directory_iterator(migrationDirectory)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + int32_t fileVersion = InternalDBManager::extractVersionFromFilename(filename); + migrations.emplace_back(fileVersion, entry.path().string()); } + } + + std::sort(migrations.begin(), migrations.end()); + + for (const auto &[fileVersion, scriptPath] : migrations) { + if (fileVersion > currentVersion) { + if (!LuaScriptInterface::reserveScriptEnv()) { + break; + } + + if (luaL_dofile(L, scriptPath.c_str()) != 0) { + g_logger().error("DatabaseManager::updateDatabase - Version: {}] {}", fileVersion, lua_tostring(L, -1)); + continue; + } + + lua_getglobal(L, "onUpdateDatabase"); + if (lua_pcall(L, 0, 1, 0) != 0) { + LuaScriptInterface::resetScriptEnv(); + g_logger().warn("[DatabaseManager::updateDatabase - Version: {}] {}", fileVersion, lua_tostring(L, -1)); + continue; + } + + currentVersion = fileVersion; + g_logger().info("Database has been updated to version {}", currentVersion); + registerDatabaseConfig("db_version", currentVersion); - if (!LuaScriptInterface::getBoolean(L, -1, false)) { LuaScriptInterface::resetScriptEnv(); - break; } + } - version++; - g_logger().info("Database has been updated to version {}", version); - registerDatabaseConfig("db_version", version); - - LuaScriptInterface::resetScriptEnv(); - } while (true); + double duration = bm.duration(); + if (duration < 1000.0) { + g_logger().debug("Database update completed in {:.2f} ms", duration); + } else { + g_logger().debug("Database update completed in {:.2f} seconds", duration / 1000.0); + } lua_close(L); } diff --git a/src/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp index af7ea1701..4d2227f8d 100644 --- a/src/enums/player_cyclopedia.hpp +++ b/src/enums/player_cyclopedia.hpp @@ -60,3 +60,61 @@ enum class CyclopediaMapData_t : uint8_t { Donations = 9, SetCurrentArea = 10, }; + +enum class CyclopediaHouseState : uint8_t { + Available = 0, + Rented = 2, + Transfer = 3, + MoveOut = 4, +}; + +enum class HouseAuctionType : uint8_t { + Bid = 1, + MoveOut = 2, + Transfer = 3, + CancelMoveOut = 4, + CancelTransfer = 5, + AcceptTransfer = 6, + RejectTransfer = 7, +}; + +enum class BidSuccessMessage : uint8_t { + BidSuccess = 0, + LowerBid = 1, +}; + +enum class BidErrorMessage : uint8_t { + NoError = 0, + Rookgaard = 3, + Premium = 5, + Guildhall = 6, + OnlyOneBid = 7, + NotEnoughMoney = 17, + NotEnoughGuildMoney = 21, + Internal = 24, +}; + +// Bytes to: +// Move Out, Transfer +// Cancel Move Out/Transfer +enum class TransferErrorMessage : uint8_t { + Success = 0, + NotHouseOwner = 2, + CharacterNotExist = 4, + Premium = 7, + Rookgaard = 16, + AlreadyTheOwner = 19, + OnlyOneBid = 25, + Internal = 32, +}; + +enum class AcceptTransferErrorMessage : uint8_t { + Success = 0, + NotNewOwner = 2, + AlreadyBid = 3, + AlreadyAccepted = 7, + Rookgaard = 8, + Premium = 9, + Frozen = 15, + Internal = 19, +}; diff --git a/src/game/game.cpp b/src/game/game.cpp index e69654a47..a4a53738b 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -72,7 +72,7 @@ #include -std::vector> checkCreatureLists[EVENT_CREATURECOUNT]; +std::vector> checkCreatureLists[EVENT_CREATURECOUNT]; namespace InternalGame { void sendBlockEffect(BlockType_t blockType, CombatType_t combatType, const Position &targetPos, const std::shared_ptr &source) { @@ -1092,7 +1092,7 @@ std::string Game::getPlayerNameByGUID(const uint32_t &guid) { ReturnValue Game::getPlayerByNameWildcard(const std::string &s, std::shared_ptr &player) { size_t strlen = s.length(); - if (strlen == 0 || strlen > 20) { + if (strlen == 0 || strlen > 29) { return RETURNVALUE_PLAYERWITHTHISNAMEISNOTONLINE; } @@ -1255,15 +1255,6 @@ bool Game::removeCreature(const std::shared_ptr &creature, bool isLogo return true; } -void Game::executeDeath(uint32_t creatureId) { - metrics::method_latency measure(__METRICS_METHOD_NAME__); - std::shared_ptr creature = getCreatureByID(creatureId); - if (creature && !creature->isRemoved()) { - afterCreatureZoneChange(creature, creature->getZones(), {}); - creature->onDeath(); - } -} - void Game::playerTeleport(uint32_t playerId, const Position &newPosition) { metrics::method_latency measure(__METRICS_METHOD_NAME__); const auto &player = getPlayerByID(playerId); @@ -5912,7 +5903,7 @@ void Game::playerSetAttackedCreature(uint32_t playerId, uint32_t creatureId) { } player->setAttackedCreature(attackCreature); - updateCreatureWalk(player->getID()); // internally uses addEventWalk. + player->updateCreatureWalk(); } void Game::playerFollowCreature(uint32_t playerId, uint32_t creatureId) { @@ -5922,7 +5913,7 @@ void Game::playerFollowCreature(uint32_t playerId, uint32_t creatureId) { } player->setAttackedCreature(nullptr); - updateCreatureWalk(player->getID()); // internally uses addEventWalk. + player->updateCreatureWalk(); player->setFollowCreature(getCreatureByID(creatureId)); } @@ -6433,27 +6424,6 @@ bool Game::internalCreatureSay(const std::shared_ptr &creature, SpeakC return true; } -void Game::checkCreatureWalk(uint32_t creatureId) { - const auto &creature = getCreatureByID(creatureId); - if (creature && creature->getHealth() > 0) { - creature->onCreatureWalk(); - } -} - -void Game::updateCreatureWalk(uint32_t creatureId) { - const auto &creature = getCreatureByID(creatureId); - if (creature && creature->getHealth() > 0) { - creature->goToFollowCreature_async(); - } -} - -void Game::checkCreatureAttack(uint32_t creatureId) { - const auto &creature = getCreatureByID(creatureId); - if (creature && creature->getHealth() > 0) { - creature->onAttacking(0); - } -} - void Game::addCreatureCheck(const std::shared_ptr &creature) { if (creature->isRemoved()) { return; @@ -6461,16 +6431,15 @@ void Game::addCreatureCheck(const std::shared_ptr &creature) { creature->creatureCheck.store(true); - if (creature->inCheckCreaturesVector.load()) { + if (creature->inCheckCreaturesVector.exchange(true)) { // already in a vector return; } - creature->inCheckCreaturesVector.store(true); - - creature->safeCall([this, creature] { - checkCreatureLists[uniform_random(0, EVENT_CREATURECOUNT - 1)].emplace_back(creature); - }); + g_dispatcher().addEvent([this, index = uniform_random(0, EVENT_CREATURECOUNT - 1), creature] { + checkCreatureLists[index].emplace_back(creature); + }, + "Game::addCreatureCheck"); } void Game::removeCreatureCheck(const std::shared_ptr &creature) { @@ -6484,15 +6453,28 @@ void Game::checkCreatures() { metrics::method_latency measure(__METRICS_METHOD_NAME__); static size_t index = 0; - std::erase_if(checkCreatureLists[index], [this](const std::shared_ptr creature) { - if (creature->creatureCheck && creature->isAlive()) { - creature->onThink(EVENT_CREATURE_THINK_INTERVAL); - creature->onAttacking(EVENT_CREATURE_THINK_INTERVAL); - creature->executeConditions(EVENT_CREATURE_THINK_INTERVAL); - return false; + std::erase_if(checkCreatureLists[index], [this](const std::weak_ptr &weak) { + if (const auto creature = weak.lock()) { + if (creature->creatureCheck && creature->isAlive()) { + creature->onThink(EVENT_CREATURE_THINK_INTERVAL); + if (creature->getMonster()) { + // The monster's onThink is executed asynchronously, + // so the target is updated later, so we need to postpone the actions below. + g_dispatcher().addEvent([creature] { + if (creature->isAlive()) { + creature->onAttacking(EVENT_CREATURE_THINK_INTERVAL); + creature->executeConditions(EVENT_CREATURE_THINK_INTERVAL); + } }, __FUNCTION__); + } else { + creature->onAttacking(EVENT_CREATURE_THINK_INTERVAL); + creature->executeConditions(EVENT_CREATURE_THINK_INTERVAL); + } + return false; + } + + creature->inCheckCreaturesVector = false; } - creature->inCheckCreaturesVector = false; return true; }); @@ -7436,7 +7418,7 @@ bool Game::combatChangeHealth(const std::shared_ptr &attacker, const s } auto targetHealth = target->getHealth(); - realDamage = damage.primary.value + damage.secondary.value; + realDamage = std::min(targetHealth, damage.primary.value + damage.secondary.value); if (realDamage == 0) { return true; } else if (realDamage >= targetHealth) { @@ -10882,3 +10864,354 @@ void Game::updatePlayersOnline() const { g_logger().error("[Game::updatePlayersOnline] Failed to update players online."); } } + +void Game::playerCyclopediaHousesByTown(uint32_t playerId, const std::string &townName) { + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + return; + } + + HouseMap houses; + if (!townName.empty()) { + const auto &housesList = g_game().map.houses.getHouses(); + for (const auto &it : housesList) { + const auto &house = it.second; + const auto &town = g_game().map.towns.getTown(house->getTownId()); + if (!town) { + return; + } + + const std::string &houseTown = town->getName(); + if (houseTown == townName) { + houses.emplace(house->getClientId(), house); + } + } + } else { + auto playerHouses = g_game().map.houses.getAllHousesByPlayerId(player->getGUID()); + if (playerHouses.size()) { + for (const auto &playerHouse : playerHouses) { + if (!playerHouse) { + continue; + } + houses.emplace(playerHouse->getClientId(), playerHouse); + } + } + + const auto house = g_game().map.houses.getHouseByBidderName(player->getName()); + if (house) { + houses.emplace(house->getClientId(), house); + } + } + player->sendCyclopediaHouseList(houses); +} + +void Game::playerCyclopediaHouseBid(uint32_t playerId, uint32_t houseId, uint64_t bidValue) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return; + } + + auto ret = player->canBidHouse(houseId); + if (ret != BidErrorMessage::NoError) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + } + ret = BidErrorMessage::NotEnoughMoney; + auto retSuccess = BidSuccessMessage::BidSuccess; + + if (house->getBidderName().empty()) { + if (!processBankAuction(player, house, bidValue)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setHighestBid(0); + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + house->setBidderName(player->getName()); + house->setBidder(player->getGUID()); + house->calculateBidEndDate(g_configManager().getNumber(DAYS_TO_CLOSE_BID)); + } else if (house->getBidderName() == player->getName()) { + if (!processBankAuction(player, house, bidValue, true)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + } else if (bidValue <= house->getInternalBid()) { + house->setHighestBid(bidValue); + retSuccess = BidSuccessMessage::LowerBid; + } else { + if (!processBankAuction(player, house, bidValue)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setHighestBid(house->getInternalBid() + 1); + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + house->setBidderName(player->getName()); + house->setBidder(player->getGUID()); + } + + const auto &town = g_game().map.towns.getTown(house->getTownId()); + if (!town) { + return; + } + + const std::string houseTown = town->getName(); + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(retSuccess), true); + playerCyclopediaHousesByTown(playerId, houseTown); +} + +void Game::playerCyclopediaHouseMoveOut(uint32_t playerId, uint32_t houseId, uint32_t timestamp) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Rented) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + house->setBidEndDate(timestamp); + house->setState(CyclopediaHouseState::MoveOut); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseCancelMoveOut(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::MoveOut) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseTransfer(uint32_t playerId, uint32_t houseId, uint32_t timestamp, const std::string &newOwnerName, uint64_t bidValue) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &owner = getPlayerByID(playerId); + if (!owner) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const std::shared_ptr &newOwner = getPlayerByName(newOwnerName, true); + if (!newOwner) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::CharacterNotExist)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Rented) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + auto ret = owner->canTransferHouse(houseId, newOwner->getGUID()); + if (ret != TransferErrorMessage::Success) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(ret)); + return; + } + + house->setBidderName(newOwnerName); + house->setBidder(newOwner->getGUID()); + house->setInternalBid(bidValue); + house->setBidEndDate(timestamp); + house->setState(CyclopediaHouseState::Transfer); + + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(ret)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseCancelTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + if (house->getTransferStatus()) { + const auto &newOwner = getPlayerByGUID(house->getBidder()); + const auto amountPaid = house->getInternalBid() + house->getRent(); + if (newOwner) { + newOwner->setBankBalance(newOwner->getBankBalance() + amountPaid); + newOwner->sendResourceBalance(RESOURCE_BANK, newOwner->getBankBalance()); + } else { + IOLoginData::increaseBankBalance(house->getBidder(), amountPaid); + } + } + + house->setBidderName(""); + house->setBidder(0); + house->setInternalBid(0); + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + house->setTransferStatus(false); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseAcceptTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Internal)); + return; + } + + auto ret = player->canAcceptTransferHouse(houseId); + if (ret != AcceptTransferErrorMessage::Success) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(ret)); + return; + } + + if (!processBankAuction(player, house, house->getInternalBid())) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Frozen)); + return; + } + + house->setTransferStatus(true); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(ret)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseRejectTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getBidder() != player->getGUID() || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + if (house->getTransferStatus()) { + const auto &newOwner = getPlayerByGUID(house->getBidder()); + const auto amountPaid = house->getInternalBid() + house->getRent(); + if (newOwner) { + newOwner->setBankBalance(newOwner->getBankBalance() + amountPaid); + newOwner->sendResourceBalance(RESOURCE_BANK, newOwner->getBankBalance()); + } else { + IOLoginData::increaseBankBalance(house->getBidder(), amountPaid); + } + } + + house->setBidderName(""); + house->setBidder(0); + house->setInternalBid(0); + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + house->setTransferStatus(false); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::RejectTransfer, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +bool Game::processBankAuction(std::shared_ptr player, const std::shared_ptr &house, uint64_t bid, bool replace /* = false*/) { + if (!replace && player->getBankBalance() < (house->getRent() + bid)) { + return false; + } + + if (player->getBankBalance() < bid) { + return false; + } + + uint64_t balance = player->getBankBalance(); + if (replace) { + player->setBankBalance(balance - (bid - house->getInternalBid())); + } else { + player->setBankBalance(balance - (house->getRent() + bid)); + } + + player->sendResourceBalance(RESOURCE_BANK, player->getBankBalance()); + + if (house->getBidderName() != player->getName()) { + const auto otherPlayer = g_game().getPlayerByName(house->getBidderName()); + if (!otherPlayer) { + uint32_t bidderGuid = IOLoginData::getGuidByName(house->getBidderName()); + IOLoginData::increaseBankBalance(bidderGuid, (house->getBidHolderLimit() + house->getRent())); + } else { + otherPlayer->setBankBalance(otherPlayer->getBankBalance() + (house->getBidHolderLimit() + house->getRent())); + otherPlayer->sendResourceBalance(RESOURCE_BANK, otherPlayer->getBankBalance()); + } + } + + return true; +} diff --git a/src/game/game.hpp b/src/game/game.hpp index b1afd8101..b16d1787d 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -173,7 +173,6 @@ class Game { bool placeCreature(const std::shared_ptr &creature, const Position &pos, bool extendedPos = false, bool force = false); bool removeCreature(const std::shared_ptr &creature, bool isLogout = true); - void executeDeath(uint32_t creatureId); void addCreatureCheck(const std::shared_ptr &creature); static void removeCreatureCheck(const std::shared_ptr &creature); @@ -290,6 +289,17 @@ class Game { void playerHighscores(const std::shared_ptr &player, HighscoreType_t type, uint8_t category, uint32_t vocation, const std::string &worldName, uint16_t page, uint8_t entriesPerPage); static std::string getSkillNameById(uint8_t &skill); + // House Auction + void playerCyclopediaHousesByTown(uint32_t playerId, const std::string &townName); + void playerCyclopediaHouseBid(uint32_t playerId, uint32_t houseId, uint64_t bidValue); + void playerCyclopediaHouseMoveOut(uint32_t playerId, uint32_t houseId, uint32_t timestamp); + void playerCyclopediaHouseCancelMoveOut(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseTransfer(uint32_t playerId, uint32_t houseId, uint32_t timestamp, const std::string &newOwnerName, uint64_t bidValue); + void playerCyclopediaHouseCancelTransfer(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseAcceptTransfer(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseRejectTransfer(uint32_t playerId, uint32_t houseId); + bool processBankAuction(std::shared_ptr player, const std::shared_ptr &house, uint64_t bid, bool replace = false); + void updatePlayerSaleItems(uint32_t playerId); bool internalStartTrade(const std::shared_ptr &player, const std::shared_ptr &partner, const std::shared_ptr &tradeItem); @@ -437,9 +447,6 @@ class Game { void setGameState(GameState_t newState); // Events - void checkCreatureWalk(uint32_t creatureId); - void updateCreatureWalk(uint32_t creatureId); - void checkCreatureAttack(uint32_t creatureId); void checkCreatures(); void checkLight(); diff --git a/src/game/scheduling/task.hpp b/src/game/scheduling/task.hpp index c01dbe2f6..c4de64059 100644 --- a/src/game/scheduling/task.hpp +++ b/src/game/scheduling/task.hpp @@ -66,15 +66,15 @@ class Task { const static std::unordered_set tasksContext = { "Decay::checkDecay", "Dispatcher::asyncEvent", - "Game::checkCreatureAttack", + "Creature::checkCreatureAttack", "Game::checkCreatureWalk", "Game::checkCreatures", "Game::checkImbuements", "Game::checkLight", "Game::createFiendishMonsters", "Game::createInfluencedMonsters", - "Game::updateCreatureWalk", "Game::updateForgeableMonsters", + "Game::addCreatureCheck", "GlobalEvents::think", "LuaEnvironment::executeTimerEvent", "Modules::executeOnRecvbyte", diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index c0a6a13f3..37ec4a4de 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -336,14 +336,6 @@ void IOLoginData::increaseBankBalance(uint32_t guid, uint64_t bankBalance) { Database::getInstance().executeQuery(query.str()); } -bool IOLoginData::hasBiddedOnHouse(uint32_t guid) { - Database &db = Database::getInstance(); - - std::ostringstream query; - query << "SELECT `id` FROM `houses` WHERE `highest_bidder` = " << guid << " LIMIT 1"; - return db.storeQuery(query.str()).get() != nullptr; -} - std::vector IOLoginData::getVIPEntries(uint32_t accountId) { std::string query = fmt::format("SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = {}", accountId); std::vector entries; diff --git a/src/io/iologindata.hpp b/src/io/iologindata.hpp index d379031cd..378e1bd23 100644 --- a/src/io/iologindata.hpp +++ b/src/io/iologindata.hpp @@ -31,7 +31,6 @@ class IOLoginData { static std::string getNameByGuid(uint32_t guid); static bool formatPlayerName(std::string &name); static void increaseBankBalance(uint32_t guid, uint64_t bankBalance); - static bool hasBiddedOnHouse(uint32_t guid); static std::vector getVIPEntries(uint32_t accountId); static void addVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify); diff --git a/src/io/iomapserialize.cpp b/src/io/iomapserialize.cpp index b1de604dd..1479197c2 100644 --- a/src/io/iomapserialize.cpp +++ b/src/io/iomapserialize.cpp @@ -273,7 +273,7 @@ void IOMapSerialize::saveTile(PropWriteStream &stream, const std::shared_ptrgetNumber("id"); const auto house = g_game().map.houses.getHouse(houseId); - if (house) { - auto owner = result->getNumber("owner"); - auto newOwner = result->getNumber("new_owner"); - // Transfer house owner - auto isTransferOnRestart = g_configManager().getBoolean(TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART); - if (isTransferOnRestart && newOwner >= 0) { + if (!house) { + continue; + } + + auto owner = result->getNumber("owner"); + auto newOwner = result->getNumber("new_owner"); + uint32_t bidder = result->getNumber("bidder"); + std::string bidderName = result->getString("bidder_name"); + uint32_t highestBid = result->getNumber("highest_bid"); + uint32_t internalBid = result->getNumber("internal_bid"); + uint32_t bidEndDate = result->getNumber("bid_end_date"); + auto state = static_cast(result->getNumber("state")); + auto transferStatus = result->getNumber("transfer_status"); + const auto timeNow = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + + // Transfer house owner + auto isTransferOnRestart = g_configManager().getBoolean(TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART); + if (isTransferOnRestart && newOwner >= 0) { + g_game().setTransferPlayerHouseItems(houseId, owner); + if (newOwner == 0) { + g_logger().debug("Removing house id '{}' owner", houseId); + house->setOwner(0); + } else { + g_logger().debug("Setting house id '{}' owner to player GUID '{}'", houseId, newOwner); + house->setOwner(newOwner); + } + } else if (state == CyclopediaHouseState::Available && timeNow > bidEndDate && bidder > 0) { + g_logger().debug("[BID] - Setting house id '{}' owner to player GUID '{}'", houseId, bidder); + if (highestBid < internalBid) { + uint32_t diff = internalBid - highestBid; + IOLoginData::increaseBankBalance(bidder, diff); + } + house->setOwner(bidder); + bidder = 0; + bidderName = ""; + highestBid = 0; + internalBid = 0; + bidEndDate = 0; + } else if (state == CyclopediaHouseState::Transfer && timeNow > bidEndDate && bidder > 0) { + g_logger().debug("[TRANSFER] - Removing house id '{}' from owner GUID '{}' and transfering to new owner GUID '{}'", houseId, owner, bidder); + if (transferStatus) { g_game().setTransferPlayerHouseItems(houseId, owner); - if (newOwner == 0) { - g_logger().debug("Removing house id '{}' owner", houseId); - house->setOwner(0); - } else { - g_logger().debug("Setting house id '{}' owner to player GUID '{}'", houseId, newOwner); - house->setOwner(newOwner); - } + house->setOwner(bidder); + IOLoginData::increaseBankBalance(owner, internalBid); } else { - house->setOwner(owner, false); + house->setOwner(owner); } - house->setPaidUntil(result->getNumber("paid")); - house->setPayRentWarnings(result->getNumber("warnings")); + bidder = 0; + bidderName = ""; + internalBid = 0; + bidEndDate = 0; + transferStatus = false; + } else if (state == CyclopediaHouseState::MoveOut && timeNow > bidEndDate) { + g_logger().debug("[MOVE OUT] - Removing house id '{}' owner", houseId); + g_game().setTransferPlayerHouseItems(houseId, owner); + house->setOwner(0); + bidEndDate = 0; + } else { + house->setOwner(owner, false); + house->setState(state); } + house->setBidder(bidder); + house->setBidderName(bidderName); + house->setHighestBid(highestBid); + house->setInternalBid(internalBid); + house->setBidHolderLimit(internalBid); + house->setBidEndDate(bidEndDate); + house->setTransferStatus(transferStatus); } while (result->next()); result = db.storeQuery("SELECT `house_id`, `listid`, `list` FROM `house_lists`"); @@ -331,11 +379,12 @@ bool IOMapSerialize::SaveHouseInfoGuard() { Database &db = Database::getInstance(); std::ostringstream query; - DBInsert houseUpdate("INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`) VALUES "); - houseUpdate.upsert({ "owner", "paid", "warnings", "name", "town_id", "rent", "size", "beds" }); + DBInsert houseUpdate("INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`, `bidder`, `bidder_name`, `highest_bid`, `internal_bid`, `bid_end_date`, `state`, `transfer_status`) VALUES "); + houseUpdate.upsert({ "owner", "paid", "warnings", "name", "town_id", "rent", "size", "beds", "bidder", "bidder_name", "highest_bid", "internal_bid", "bid_end_date", "state", "transfer_status" }); for (const auto &[key, house] : g_game().map.houses.getHouses()) { - std::string values = fmt::format("{},{},{},{},{},{},{},{},{}", house->getId(), house->getOwner(), house->getPaidUntil(), house->getPayRentWarnings(), db.escapeString(house->getName()), house->getTownId(), house->getRent(), house->getSize(), house->getBedCount()); + auto stateValue = magic_enum::enum_integer(house->getState()); + std::string values = fmt::format("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", house->getId(), house->getOwner(), house->getPaidUntil(), house->getPayRentWarnings(), db.escapeString(house->getName()), house->getTownId(), house->getRent(), house->getSize(), house->getBedCount(), house->getBidder(), db.escapeString(house->getBidderName()), house->getHighestBid(), house->getInternalBid(), house->getBidEndDate(), std::to_string(stateValue), (house->getTransferStatus() ? 1 : 0)); if (!houseUpdate.addRow(values)) { return false; diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index aad4b71d0..eec876eee 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -524,7 +524,7 @@ int GameFunctions::luaGameCreateMonster(lua_State* L) { const bool extended = Lua::getBoolean(L, 3, false); const bool force = Lua::getBoolean(L, 4, false); if (g_game().placeCreature(monster, position, extended, force)) { - monster->onSpawn(); + monster->onSpawn(position); const auto &mtype = monster->getMonsterType(); if (mtype && mtype->info.raceid > 0 && mtype->info.bosstiaryRace == BosstiaryRarity_t::RARITY_ARCHFOE) { for (const auto &spectator : Spectators().find(monster->getPosition(), true)) { diff --git a/src/lua/functions/creatures/player/party_functions.cpp b/src/lua/functions/creatures/player/party_functions.cpp index 5c4359328..a44e0129b 100644 --- a/src/lua/functions/creatures/player/party_functions.cpp +++ b/src/lua/functions/creatures/player/party_functions.cpp @@ -24,6 +24,7 @@ void PartyFunctions::init(lua_State* L) { Lua::registerMethod(L, "Party", "getMemberCount", PartyFunctions::luaPartyGetMemberCount); Lua::registerMethod(L, "Party", "getInvitees", PartyFunctions::luaPartyGetInvitees); Lua::registerMethod(L, "Party", "getInviteeCount", PartyFunctions::luaPartyGetInviteeCount); + Lua::registerMethod(L, "Party", "getUniqueVocationsCount", PartyFunctions::luaPartyGetUniqueVocationsCount); Lua::registerMethod(L, "Party", "addInvite", PartyFunctions::luaPartyAddInvite); Lua::registerMethod(L, "Party", "removeInvite", PartyFunctions::luaPartyRemoveInvite); Lua::registerMethod(L, "Party", "addMember", PartyFunctions::luaPartyAddMember); @@ -162,6 +163,17 @@ int PartyFunctions::luaPartyGetInviteeCount(lua_State* L) { return 1; } +int PartyFunctions::luaPartyGetUniqueVocationsCount(lua_State* L) { + // party:getUniqueVocationsCount() + const auto &party = Lua::getUserdataShared(L, 1); + if (party) { + lua_pushnumber(L, party->getUniqueVocationsCount()); + } else { + lua_pushnil(L); + } + return 1; +} + int PartyFunctions::luaPartyAddInvite(lua_State* L) { // party:addInvite(player) const auto &player = Lua::getPlayer(L, 2); diff --git a/src/lua/functions/creatures/player/party_functions.hpp b/src/lua/functions/creatures/player/party_functions.hpp index 52801e349..92e629f71 100644 --- a/src/lua/functions/creatures/player/party_functions.hpp +++ b/src/lua/functions/creatures/player/party_functions.hpp @@ -22,6 +22,7 @@ class PartyFunctions { static int luaPartyGetMemberCount(lua_State* L); static int luaPartyGetInvitees(lua_State* L); static int luaPartyGetInviteeCount(lua_State* L); + static int luaPartyGetUniqueVocationsCount(lua_State* L); static int luaPartyAddInvite(lua_State* L); static int luaPartyRemoveInvite(lua_State* L); static int luaPartyAddMember(lua_State* L); diff --git a/src/lua/functions/items/item_type_functions.cpp b/src/lua/functions/items/item_type_functions.cpp index 1669e6b79..9c12f80e5 100644 --- a/src/lua/functions/items/item_type_functions.cpp +++ b/src/lua/functions/items/item_type_functions.cpp @@ -283,6 +283,17 @@ int ItemTypeFunctions::luaItemTypeIsQuiver(lua_State* L) { return 1; } +int ItemTypeFunctions::luaItemTypeIsPodium(lua_State* L) { + // itemType:isPodium() + const auto* itemType = Lua::getUserdata(L, 1); + if (itemType) { + Lua::pushBoolean(L, itemType->isPodium); + } else { + lua_pushnil(L); + } + return 1; +} + int ItemTypeFunctions::luaItemTypeGetType(lua_State* L) { // itemType:getType() const auto* itemType = Lua::getUserdata(L, 1); diff --git a/src/lua/functions/items/item_type_functions.hpp b/src/lua/functions/items/item_type_functions.hpp index 19a401bc2..fa17f5757 100644 --- a/src/lua/functions/items/item_type_functions.hpp +++ b/src/lua/functions/items/item_type_functions.hpp @@ -35,6 +35,7 @@ class ItemTypeFunctions { static int luaItemTypeIsPickupable(lua_State* L); static int luaItemTypeIsKey(lua_State* L); static int luaItemTypeIsQuiver(lua_State* L); + static int luaItemTypeIsPodium(lua_State* L); static int luaItemTypeGetType(lua_State* L); static int luaItemTypeGetId(lua_State* L); diff --git a/src/lua/functions/map/house_functions.cpp b/src/lua/functions/map/house_functions.cpp index 10ebbc2cf..dd20d6fdc 100644 --- a/src/lua/functions/map/house_functions.cpp +++ b/src/lua/functions/map/house_functions.cpp @@ -9,6 +9,7 @@ #include "lua/functions/map/house_functions.hpp" +#include "account/account.hpp" #include "config/configmanager.hpp" #include "items/bed.hpp" #include "game/game.hpp" @@ -238,7 +239,7 @@ int HouseFunctions::luaHouseStartTrade(lua_State* L) { return 1; } - if (IOLoginData::hasBiddedOnHouse(tradePartner->getGUID())) { + if (tradePartner->getAccount()->getHouseBidId() != 0) { lua_pushnumber(L, RETURNVALUE_TRADEPLAYERHIGHESTBIDDER); return 1; } diff --git a/src/map/house/house.cpp b/src/map/house/house.cpp index 6d9356117..b3054616b 100644 --- a/src/map/house/house.cpp +++ b/src/map/house/house.cpp @@ -95,7 +95,7 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: Database &db = Database::getInstance(); std::ostringstream query; - query << "UPDATE `houses` SET `owner` = " << guid << ", `new_owner` = -1, `bid` = 0, `bid_end` = 0, `last_bid` = 0, `highest_bidder` = 0 WHERE `id` = " << id; + query << "UPDATE `houses` SET `owner` = " << guid << ", `new_owner` = -1, `paid` = 0, `bidder` = 0, `bidder_name` = '', `highest_bid` = 0, `internal_bid` = 0, `bid_end_date` = 0, `state` = " << (guid > 0 ? 2 : 0) << " WHERE `id` = " << id; db.executeQuery(query.str()); } @@ -107,7 +107,9 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: if (owner != 0) { tryTransferOwnership(player, false); - } else { + } + + if (guid != 0) { std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); time_t currentTime = time(nullptr); if (strRentPeriod == "yearly") { @@ -123,6 +125,8 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: } paidUntil = currentTime; + } else { + paidUntil = 0; } rentWarnings = 0; @@ -141,6 +145,7 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: owner = guid; ownerName = name; ownerAccountId = result->getNumber("account_id"); + m_state = CyclopediaHouseState::Rented; } } @@ -155,15 +160,17 @@ void House::updateDoorDescription() const { ss << "It belongs to house '" << houseName << "'. Nobody owns this house."; } - ss << " It is " << getSize() << " square meters."; - const int32_t housePrice = getPrice(); - if (housePrice != -1) { - if (g_configManager().getBoolean(HOUSE_PURSHASED_SHOW_PRICE) || owner == 0) { - ss << " It costs " << formatNumber(getPrice()) << " gold coins."; - } - std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); - if (strRentPeriod != "never") { - ss << " The rent cost is " << formatNumber(getRent()) << " gold coins and it is billed " << strRentPeriod << "."; + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + ss << " It is " << getSize() << " square meters."; + const int32_t housePrice = getPrice(); + if (housePrice != -1) { + if (g_configManager().getBoolean(HOUSE_PURSHASED_SHOW_PRICE) || owner == 0) { + ss << " It costs " << formatNumber(getPrice()) << " gold coins."; + } + std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); + if (strRentPeriod != "never") { + ss << " The rent cost is " << formatNumber(getRent()) << " gold coins and it is billed " << strRentPeriod << "."; + } } } @@ -479,6 +486,43 @@ void House::resetTransferItem() { } } +void House::calculateBidEndDate(uint8_t daysToEnd) { + auto currentTimeMs = std::chrono::system_clock::now().time_since_epoch(); + + auto now = std::chrono::system_clock::time_point( + std::chrono::duration_cast(currentTimeMs) + ); + + // Truncate to whole days since epoch + days daysSinceEpoch = std::chrono::duration_cast(now.time_since_epoch()); + + // Get today's date at 00:00:00 UTC + auto todayMidnight = std::chrono::system_clock::time_point(daysSinceEpoch); + + std::chrono::system_clock::time_point targetDay = todayMidnight + days(daysToEnd); + + const auto serverSaveTime = g_configManager().getString(GLOBAL_SERVER_SAVE_TIME); + + std::vector params = vectorAtoi(explodeString(serverSaveTime, ":")); + int32_t hour = params.front(); + int32_t min = 0; + int32_t sec = 0; + if (params.size() > 1) { + min = params[1]; + + if (params.size() > 2) { + sec = params[2]; + } + } + std::chrono::system_clock::time_point targetTime = targetDay + std::chrono::hours(hour) + std::chrono::minutes(min) + std::chrono::seconds(sec); + + std::time_t resultTime = std::chrono::system_clock::to_time_t(targetTime); + std::tm* localTime = std::localtime(&resultTime); + auto bidEndDate = static_cast(std::mktime(localTime)); + + this->m_bidEndDate = bidEndDate; +} + std::shared_ptr HouseTransferItem::createHouseTransferItem(const std::shared_ptr &house) { auto transferItem = std::make_shared(house); transferItem->setID(ITEM_DOCUMENT_RO); @@ -725,6 +769,35 @@ std::shared_ptr Houses::getHouseByPlayerId(uint32_t playerId) const { return nullptr; } +std::vector> Houses::getAllHousesByPlayerId(uint32_t playerId) { + std::vector> playerHouses; + for (const auto &[id, house] : houseMap) { + if (house->getOwner() == playerId) { + playerHouses.emplace_back(house); + } + } + return playerHouses; +} + +std::shared_ptr Houses::getHouseByBidderName(const std::string &bidderName) { + for (const auto &[id, house] : houseMap) { + if (house->getBidderName() == bidderName) { + return house; + } + } + return nullptr; +} + +uint16_t Houses::getHouseCountByAccount(uint32_t accountId) { + uint16_t count = 0; + for (const auto &[id, house] : houseMap) { + if (house->getOwnerAccountId() == accountId) { + ++count; + } + } + return count; +} + bool Houses::loadHousesXML(const std::string &filename) { pugi::xml_document doc; const pugi::xml_parse_result result = doc.load_file(filename.c_str()); @@ -764,6 +837,13 @@ bool Houses::loadHousesXML(const std::string &filename) { house->setRent(pugi::cast(houseNode.attribute("rent").value())); house->setSize(pugi::cast(houseNode.attribute("size").value())); house->setTownId(pugi::cast(houseNode.attribute("townid").value())); + house->setClientId(pugi::cast(houseNode.attribute("clientid").value())); + + auto guildhallAttr = houseNode.attribute("guildhall"); + if (!guildhallAttr.empty()) { + house->setGuildhall(static_cast(guildhallAttr.as_bool())); + } + auto maxBedsAttr = houseNode.attribute("beds"); int32_t maxBeds = -1; if (!maxBedsAttr.empty()) { @@ -772,6 +852,7 @@ bool Houses::loadHousesXML(const std::string &filename) { house->setMaxBeds(maxBeds); house->setOwner(0, false); + addHouseClientId(house->getClientId(), house); } return true; } diff --git a/src/map/house/house.hpp b/src/map/house/house.hpp index a3dc76598..d994fdbe3 100644 --- a/src/map/house/house.hpp +++ b/src/map/house/house.hpp @@ -13,11 +13,14 @@ #include "declarations.hpp" #include "map/house/housetile.hpp" #include "game/movement/position.hpp" +#include "enums/player_cyclopedia.hpp" class House; class BedItem; class Player; +using days = std::chrono::duration>; + class AccessList { public: void parseList(const std::string &list); @@ -233,6 +236,84 @@ class House final : public SharedObject { bool hasNewOwnership() const; void setNewOwnership(); + void setClientId(uint32_t newClientId) { + this->m_clientId = newClientId; + } + uint32_t getClientId() const { + return m_clientId; + } + + void setBidder(int32_t bidder) { + this->m_bidder = bidder; + } + int32_t getBidder() const { + return m_bidder; + } + + void setBidderName(const std::string &bidderName) { + this->m_bidderName = bidderName; + } + std::string getBidderName() const { + return m_bidderName; + } + + void setHighestBid(uint64_t bidValue) { + this->m_highestBid = bidValue; + } + uint64_t getHighestBid() const { + return m_highestBid; + } + + void setInternalBid(uint64_t bidValue) { + this->m_internalBid = bidValue; + } + uint64_t getInternalBid() const { + return m_internalBid; + } + + void setBidHolderLimit(uint64_t bidValue) { + this->m_bidHolderLimit = bidValue; + } + uint64_t getBidHolderLimit() const { + return m_bidHolderLimit; + } + + void calculateBidEndDate(uint8_t daysToEnd); + void setBidEndDate(uint32_t bidEndDate) { + this->m_bidEndDate = bidEndDate; + }; + uint32_t getBidEndDate() const { + return m_bidEndDate; + } + + void setState(CyclopediaHouseState state) { + this->m_state = state; + } + CyclopediaHouseState getState() const { + return m_state; + } + + void setTransferStatus(bool transferStatus) { + this->m_transferStatus = transferStatus; + } + bool getTransferStatus() const { + return m_transferStatus; + } + + void setOwnerAccountId(uint32_t accountId) { + this->ownerAccountId = accountId; + } + uint32_t getOwnerAccountId() const { + return ownerAccountId; + } + + void setGuildhall(bool isGuildHall) { + this->guildHall = isGuildHall; + } + bool isGuildhall() const { + return guildHall; + } + private: bool transferToDepot() const; @@ -263,9 +344,21 @@ class House final : public SharedObject { uint32_t townId = 0; uint32_t maxBeds = 4; int32_t bedsCount = -1; + bool guildHall = false; Position posEntry = {}; + // House Auction + uint32_t m_clientId; + int32_t m_bidder = 0; + std::string m_bidderName = ""; + uint64_t m_highestBid = 0; + uint64_t m_internalBid = 0; + uint64_t m_bidHolderLimit = 0; + uint32_t m_bidEndDate = 0; + CyclopediaHouseState m_state = CyclopediaHouseState::Available; + bool m_transferStatus = false; + bool isLoaded = false; void handleContainer(ItemList &moveItemList, const std::shared_ptr &item) const; @@ -299,7 +392,26 @@ class Houses { return it->second; } + void addHouseClientId(uint32_t clientId, std::shared_ptr house) { + if (auto it = houseMapClientId.find(clientId); it != houseMapClientId.end()) { + return; + } + + houseMapClientId.emplace(clientId, house); + } + + std::shared_ptr getHouseByClientId(uint32_t clientId) { + auto it = houseMapClientId.find(clientId); + if (it == houseMapClientId.end()) { + return nullptr; + } + return it->second; + } + std::shared_ptr getHouseByPlayerId(uint32_t playerId) const; + std::vector> getAllHousesByPlayerId(uint32_t playerId); + std::shared_ptr getHouseByBidderName(const std::string &bidderName); + uint16_t getHouseCountByAccount(uint32_t accountId); bool loadHousesXML(const std::string &filename); @@ -311,4 +423,5 @@ class Houses { private: HouseMap houseMap; + HouseMap houseMapClientId; }; diff --git a/src/security/rsa.cpp b/src/security/rsa.cpp index 18e1f6e83..f148ae6d9 100644 --- a/src/security/rsa.cpp +++ b/src/security/rsa.cpp @@ -170,11 +170,17 @@ uint16_t RSA::decodeLength(char*&pos) const { if (length & 0x80) { uint8_t numLengthBytes = length & 0x7F; if (numLengthBytes > 4) { - g_logger().error("[RSA::loadPEM] - Invalid 'length'"); + g_logger().error("[RSA::decodeLength] - Invalid 'length'"); return 0; } - // Copy 'numLengthBytes' bytes from 'pos' into 'buffer', starting at the correct position - std::ranges::copy_n(pos, numLengthBytes, buffer.begin() + (4 - numLengthBytes)); + // Adjust the copy destination to ensure it doesn't overflow + auto destIt = buffer.begin() + (4 - numLengthBytes); + if (destIt < buffer.begin() || destIt + numLengthBytes > buffer.end()) { + g_logger().error("[RSA::decodeLength] - Invalid copy range"); + return 0; + } + // Copy 'numLengthBytes' bytes from 'pos' into 'buffer' + std::copy_n(pos, numLengthBytes, destIt); pos += numLengthBytes; // Reconstruct 'length' from 'buffer' (big-endian) uint32_t tempLength = 0; @@ -182,7 +188,7 @@ uint16_t RSA::decodeLength(char*&pos) const { tempLength = (tempLength << 8) | buffer[4 - numLengthBytes + i]; } if (tempLength > UINT16_MAX) { - g_logger().error("[RSA::loadPEM] - Length too large"); + g_logger().error("[RSA::decodeLength] - Length too large"); return 0; } length = static_cast(tempLength); diff --git a/src/server/network/message/networkmessage.cpp b/src/server/network/message/networkmessage.cpp index 0fb8b6332..4f203dc45 100644 --- a/src/server/network/message/networkmessage.cpp +++ b/src/server/network/message/networkmessage.cpp @@ -45,10 +45,12 @@ int32_t NetworkMessage::decodeHeader() { } // Simply read functions for incoming message -uint8_t NetworkMessage::getByte(const std::source_location &location /*= std::source_location::current()*/) { +uint8_t NetworkMessage::getByte(bool suppresLog /*= false*/, const std::source_location &location /*= std::source_location::current()*/) { // Check if there is at least 1 byte to read if (!canRead(1)) { - g_logger().error("[{}] Not enough data to read a byte. Current position: {}, Length: {}. Called line {}:{} in {}", __FUNCTION__, info.position, info.length, location.line(), location.column(), location.function_name()); + if (!suppresLog) { + g_logger().error("[{}] Not enough data to read a byte. Current position: {}, Length: {}. Called line {}:{} in {}", __FUNCTION__, info.position, info.length, location.line(), location.column(), location.function_name()); + } return {}; } @@ -113,7 +115,7 @@ Position NetworkMessage::getPosition() { Position pos; pos.x = get(); pos.y = get(); - pos.z = getByte(); + pos.z = getByte(true); return pos; } diff --git a/src/server/network/message/networkmessage.hpp b/src/server/network/message/networkmessage.hpp index f738de850..6549eec8e 100644 --- a/src/server/network/message/networkmessage.hpp +++ b/src/server/network/message/networkmessage.hpp @@ -34,7 +34,7 @@ class NetworkMessage { } // simply read functions for incoming message - uint8_t getByte(const std::source_location &location = std::source_location::current()); + uint8_t getByte(bool suppresLog = false, const std::source_location &location = std::source_location::current()); uint8_t getPreviousByte(); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 2f7ff9e24..60e298474 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -55,6 +55,7 @@ #include "enums/account_type.hpp" #include "enums/object_category.hpp" #include "enums/player_blessings.hpp" +#include "enums/player_cyclopedia.hpp" /* * NOTE: This namespace is used so that we can add functions without having to declare them in the ".hpp/.hpp" file @@ -1227,6 +1228,9 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby case 0xAC: parseChannelExclude(msg); break; + case 0xAD: + parseCyclopediaHouseAuction(msg); + break; case 0xAE: parseSendBosstiary(); break; @@ -1689,7 +1693,7 @@ void ProtocolGame::parseSetOutfit(NetworkMessage &msg) { g_logger().debug("Bool isMounted: {}", isMounted); } - uint8_t isMountRandomized = msg.getByte(); + uint8_t isMountRandomized = !oldProtocol ? msg.getByte() : 0; g_game().playerChangeOutfit(player->getID(), newOutfit, isMountRandomized); } else if (outfitType == 1) { // This value probably has something to do with try outfit variable inside outfit window dialog @@ -1713,14 +1717,14 @@ void ProtocolGame::parseSetOutfit(NetworkMessage &msg) { } void ProtocolGame::parseToggleMount(NetworkMessage &msg) { - bool mount = msg.getByte() != 0; + bool mount = msg.getByte(true) != 0; g_game().playerToggleMount(player->getID(), mount); } void ProtocolGame::parseApplyImbuement(NetworkMessage &msg) { uint8_t slot = msg.getByte(); auto imbuementId = msg.get(); - bool protectionCharm = msg.getByte() != 0x00; + bool protectionCharm = msg.getByte(true) != 0x00; g_game().playerApplyImbuement(player->getID(), imbuementId, slot, protectionCharm); } @@ -1967,8 +1971,8 @@ void ProtocolGame::parsePlayerBuyOnShop(NetworkMessage &msg) { auto id = msg.get(); uint8_t count = msg.getByte(); uint16_t amount = oldProtocol ? static_cast(msg.getByte()) : msg.get(); - bool ignoreCap = msg.getByte() != 0; - bool inBackpacks = msg.getByte() != 0; + bool ignoreCap = msg.getByte(true) != 0; + bool inBackpacks = msg.getByte(true) != 0; g_game().playerBuyItem(player->getID(), id, count, amount, ignoreCap, inBackpacks); } @@ -1976,7 +1980,7 @@ void ProtocolGame::parsePlayerSellOnShop(NetworkMessage &msg) { auto id = msg.get(); uint8_t count = std::max(msg.getByte(), (uint8_t)1); uint16_t amount = oldProtocol ? static_cast(msg.getByte()) : msg.get(); - bool ignoreEquipped = msg.getByte() != 0; + bool ignoreEquipped = msg.getByte(true) != 0; g_game().playerSellItem(player->getID(), id, count, amount, ignoreEquipped); } @@ -2010,7 +2014,7 @@ void ProtocolGame::parseEditVip(NetworkMessage &msg) { auto guid = msg.get(); const std::string description = msg.getString(); uint32_t icon = std::min(10, msg.get()); // 10 is max icon in 9.63 - bool notify = msg.getByte() != 0; + bool notify = msg.getByte(true) != 0; uint8_t groupsAmount = msg.getByte(); for (uint8_t i = 0; i < groupsAmount; ++i) { uint8_t groupId = msg.getByte(); @@ -2176,7 +2180,7 @@ void ProtocolGame::parseTaskHuntingAction(NetworkMessage &msg) { uint8_t slot = msg.getByte(); uint8_t action = msg.getByte(); - bool upgrade = msg.getByte() != 0; + bool upgrade = msg.getByte(true) != 0; auto raceId = msg.get(); if (!g_configManager().getBoolean(TASK_HUNTING_ENABLED)) { @@ -3141,7 +3145,7 @@ void ProtocolGame::parseMarketCreateOffer(NetworkMessage &msg) { auto amount = msg.get(); uint64_t price = oldProtocol ? static_cast(msg.get()) : msg.get(); - bool anonymous = (msg.getByte() != 0); + bool anonymous = (msg.getByte(true) != 0); if (amount > 0 && price > 0) { g_game().playerCreateMarketOffer(player->getID(), type, itemId, amount, price, itemTier, anonymous); } @@ -3247,12 +3251,6 @@ void ProtocolGame::sendCreatureOutfit(const std::shared_ptr &creature, msg.add(creature->getID()); AddOutfit(msg, newOutfit); - if (!oldProtocol && newOutfit.lookMount != 0) { - msg.addByte(newOutfit.lookMountHead); - msg.addByte(newOutfit.lookMountBody); - msg.addByte(newOutfit.lookMountLegs); - msg.addByte(newOutfit.lookMountFeet); - } writeToOutputBuffer(msg); } @@ -6911,6 +6909,7 @@ void ProtocolGame::sendAddCreature(const std::shared_ptr &creature, co sendLootContainers(); sendBasicData(); + sendHousesInfo(); // Wheel of destiny cooldown if (!oldProtocol && g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) { player->wheel()->sendGiftOfLifeCooldown(); @@ -7184,10 +7183,12 @@ void ProtocolGame::sendOutfitWindow() { return; } - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountHead); - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountBody); - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountLegs); - msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountFeet); + if (currentOutfit.lookMount == 0) { + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountHead); + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountBody); + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountLegs); + msg.addByte(isSupportOutfit ? 0 : currentOutfit.lookMountFeet); + } msg.add(currentOutfit.lookFamiliarsType); auto startOutfits = msg.getBufferPosition(); @@ -7750,12 +7751,6 @@ void ProtocolGame::AddCreature(NetworkMessage &msg, const std::shared_ptrisInGhostMode() && !creature->isInvisible()) { const Outfit_t &outfit = creature->getCurrentOutfit(); AddOutfit(msg, outfit); - if (!oldProtocol && outfit.lookMount != 0) { - msg.addByte(outfit.lookMountHead); - msg.addByte(outfit.lookMountBody); - msg.addByte(outfit.lookMountLegs); - msg.addByte(outfit.lookMountFeet); - } } else { static Outfit_t outfit; AddOutfit(msg, outfit); @@ -7945,6 +7940,12 @@ void ProtocolGame::AddOutfit(NetworkMessage &msg, const Outfit_t &outfit, bool a if (addMount) { msg.add(outfit.lookMount); + if (!oldProtocol && outfit.lookMount != 0) { + msg.addByte(outfit.lookMountHead); + msg.addByte(outfit.lookMountBody); + msg.addByte(outfit.lookMountLegs); + msg.addByte(outfit.lookMountFeet); + } } } @@ -9345,3 +9346,192 @@ void ProtocolGame::sendTakeScreenshot(Screenshot_t screenshotType) { msg.addByte(screenshotType); writeToOutputBuffer(msg); } + +void ProtocolGame::parseCyclopediaHouseAuction(NetworkMessage &msg) { + if (oldProtocol) { + return; + } + + uint8_t houseActionType = msg.getByte(); + switch (houseActionType) { + case 0: { + const auto townName = msg.getString(); + g_game().playerCyclopediaHousesByTown(player->getID(), townName); + break; + } + case 1: { + const uint32_t houseId = msg.get(); + const uint64_t bidValue = msg.get(); + g_game().playerCyclopediaHouseBid(player->getID(), houseId, bidValue); + break; + } + case 2: { + const uint32_t houseId = msg.get(); + const uint32_t timestamp = msg.get(); + g_game().playerCyclopediaHouseMoveOut(player->getID(), houseId, timestamp); + break; + } + case 3: { + const uint32_t houseId = msg.get(); + const uint32_t timestamp = msg.get(); + const std::string &newOwner = msg.getString(); + const uint64_t bidValue = msg.get(); + g_game().playerCyclopediaHouseTransfer(player->getID(), houseId, timestamp, newOwner, bidValue); + break; + } + case 4: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseCancelMoveOut(player->getID(), houseId); + break; + } + case 5: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseCancelTransfer(player->getID(), houseId); + break; + } + case 6: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseAcceptTransfer(player->getID(), houseId); + break; + } + case 7: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseRejectTransfer(player->getID(), houseId); + break; + } + } +} + +void ProtocolGame::sendCyclopediaHouseList(HouseMap houses) { + NetworkMessage msg; + msg.addByte(0xC7); + msg.add(houses.size()); + for (const auto &[clientId, houseData] : houses) { + msg.add(clientId); + msg.addByte(0x01); // 0x00 = Renovation; 0x01 = Available + + auto houseState = houseData->getState(); + auto stateValue = magic_enum::enum_integer(houseState); + msg.addByte(stateValue); + if (houseState == CyclopediaHouseState::Available) { + bool bidder = houseData->getBidderName() == player->getName(); + msg.addString(houseData->getBidderName()); + msg.addByte(bidder); + uint8_t disableIndex = enumToValue(player->canBidHouse(clientId)); + msg.addByte(disableIndex); + + if (!houseData->getBidderName().empty()) { + msg.add(houseData->getBidEndDate()); + msg.add(houseData->getHighestBid()); + if (bidder) { + msg.add(houseData->getBidHolderLimit()); + } + } + } else if (houseState == CyclopediaHouseState::Rented) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool rented = ownerName.compare(player->getName()) == 0; + msg.addByte(rented); + if (rented) { + msg.addByte(0); + msg.addByte(0); + } + } else if (houseState == CyclopediaHouseState::Transfer) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool isOwner = ownerName.compare(player->getName()) == 0; + msg.addByte(isOwner); + if (isOwner) { + msg.addByte(0); // ? + msg.addByte(0); // ? + } + msg.add(houseData->getBidEndDate()); + msg.addString(houseData->getBidderName()); + msg.addByte(0); // ? + msg.add(houseData->getInternalBid()); + + bool isNewOwner = player->getName() == houseData->getBidderName(); + msg.addByte(isNewOwner); + if (isNewOwner) { + uint8_t disableIndex = enumToValue(player->canAcceptTransferHouse(clientId)); + msg.addByte(disableIndex); // Accept Transfer Error + msg.addByte(0); // Reject Transfer Error + } + + if (isOwner) { + msg.addByte(0); // Cancel Transfer Error + } + } else if (houseState == CyclopediaHouseState::MoveOut) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool isOwner = ownerName.compare(player->getName()) == 0; + msg.addByte(isOwner); + if (isOwner) { + msg.addByte(0); // ? + msg.addByte(0); // ? + msg.add(houseData->getBidEndDate()); + msg.addByte(0); + } else { + msg.add(houseData->getBidEndDate()); + } + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess /* = false*/) { + NetworkMessage msg; + const auto typeValue = enumToValue(type); + + msg.addByte(0xC3); + msg.add(houseId); + msg.addByte(typeValue); + if (bidSuccess && typeValue == 1) { + msg.addByte(0x00); + } + msg.addByte(index); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHousesInfo() { + NetworkMessage msg; + + uint32_t houseClientId = 0; + const auto accountHouseCount = g_game().map.houses.getHouseCountByAccount(player->getAccountId()); + const auto house = g_game().map.houses.getHouseByPlayerId(player->getGUID()); + if (house) { + houseClientId = house->getClientId(); + } + + msg.addByte(0xC6); + msg.add(houseClientId); + msg.addByte(0x00); + + msg.addByte(accountHouseCount); // Houses Account + + msg.addByte(0x00); + + msg.addByte(3); + msg.addByte(3); + + msg.addByte(0x01); + + msg.addByte(0x01); + msg.add(houseClientId); + + const auto &housesList = g_game().map.houses.getHouses(); + msg.add(housesList.size()); + for (const auto &it : housesList) { + msg.add(it.second->getClientId()); + } + + writeToOutputBuffer(msg); +} diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 2048bea7c..7b7e0aace 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -29,6 +29,7 @@ enum Slots_t : uint8_t; enum CombatType_t : uint8_t; enum SoundEffect_t : uint16_t; enum class SourceEffect_t : uint8_t; +enum class HouseAuctionType : uint8_t; class NetworkMessage; class Player; @@ -68,6 +69,7 @@ using MarketOfferList = std::list; using HistoryMarketOfferList = std::list; using ItemsTierCountList = std::map>; using StashItemList = std::map; +using HouseMap = std::map>; struct TextMessage { TextMessage() = default; @@ -353,6 +355,11 @@ class ProtocolGame final : public Protocol { void sendCyclopediaCharacterBadges(); void sendCyclopediaCharacterTitles(); + void sendHousesInfo(); + void parseCyclopediaHouseAuction(NetworkMessage &msg); + void sendCyclopediaHouseList(HouseMap houses); + void sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess); + void sendCreatureWalkthrough(const std::shared_ptr &creature, bool walkthrough); void sendCreatureShield(const std::shared_ptr &creature); void sendCreatureEmblem(const std::shared_ptr &creature);