Skip to content

Commit

Permalink
feat/improve: create "custom loot" for monsters
Browse files Browse the repository at this point in the history
This has two different options:
Adding "global" items that can drop on any monster (it is disabled by default)

Add specific items for each monster, without having to modify each monster's script. This allows a certain freedom to work with loot, without having to edit each script.

Added "const ref" to getBestiaryList, avoid copying large bestiary map.

Improved "registerLoot" function to avoid code duplication.
Removed "bad" code in "closeContainer", this may crash if the container is used (it is removed right before)

New lua function "getMonsterTypeByName".
  • Loading branch information
dudantas committed Nov 8, 2024
1 parent e6c0995 commit 34db7f7
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 142 deletions.
200 changes: 74 additions & 126 deletions data/scripts/lib/register_monster_type.lua
Original file line number Diff line number Diff line change
Expand Up @@ -344,140 +344,88 @@ function SortLootByChance(loot)
end)
end

registerMonsterType.loot = function(mtype, mask)
if type(mask.loot) == "table" then
SortLootByChance(mask.loot)
local lootError = false
for _, loot in pairs(mask.loot) do
local parent = Loot()
if loot.name then
if not parent:setIdFromName(loot.name) then
lootError = true
end
else
if not isInteger(loot.id) or loot.id < 1 then
lootError = true
end
parent:setId(loot.id)
end
if loot.subType or loot.charges then
parent:setSubType(loot.subType or loot.charges)
else
local lType = ItemType(loot.name and loot.name or loot.id)
if lType and lType:getCharges() > 1 then
parent:setSubType(lType:getCharges())
end
end
if loot.chance then
parent:setChance(loot.chance)
end
if loot.minCount then
parent:setMinCount(loot.minCount)
end
if loot.maxCount then
parent:setMaxCount(loot.maxCount)
end
if loot.aid or loot.actionId then
parent:setActionId(loot.aid or loot.actionId)
end
if loot.text or loot.description then
parent:setText(loot.text or loot.description)
end
if loot.name then
parent:setNameItem(loot.name)
end
if loot.article then
parent:setArticle(loot.article)
end
if loot.attack then
parent:setAttack(loot.attack)
end
if loot.defense then
parent:setDefense(loot.defense)
end
if loot.extraDefense or loot.extraDef then
parent:setExtraDefense(loot.extraDefense or loot.extraDef)
end
if loot.armor then
parent:setArmor(loot.armor)
local function configureLootAttributes(lootObject, lootProperties)
if lootProperties.subType or lootProperties.charges then
lootObject:setSubType(lootProperties.subType or lootProperties.charges)
else
local itemType = ItemType(lootProperties.name and lootProperties.name or lootProperties.itemId)
if itemType and itemType:getCharges() > 1 then
lootObject:setSubType(itemType:getCharges())
end
end

lootObject:setChance(lootProperties.chance or 0)
lootObject:setMinCount(lootProperties.minCount or 0)
lootObject:setMaxCount(lootProperties.maxCount or 0)
lootObject:setActionId(lootProperties.aid or lootProperties.actionId or 0)
lootObject:setText(lootProperties.text or lootProperties.description or "")
lootObject:setNameItem(lootProperties.name or "")
lootObject:setArticle(lootProperties.article or "")
lootObject:setAttack(lootProperties.attack or 0)
lootObject:setDefense(lootProperties.defense or 0)
lootObject:setExtraDefense(lootProperties.extraDefense or lootProperties.extraDef or 0)
lootObject:setArmor(lootProperties.armor or 0)
lootObject:setShootRange(lootProperties.shootRange or lootProperties.range or 0)
lootObject:setUnique(lootProperties.unique or false)
end

local function addChildrenLoot(parent, childrenLoot)
SortLootByChance(childrenLoot)
for _, child in pairs(childrenLoot) do
local childLoot = Loot()
if child.name then
if not childLoot:setIdFromName(child.name) then
return true
end
if loot.shootRange or loot.range then
parent:setShootRange(loot.shootRange or loot.range)
else
if not isInteger(child.id) or child.id < 1 then
return true
end
if loot.unique then
parent:setUnique(loot.unique)
childLoot:setId(child.id)
end
configureLootAttributes(childLoot, child)
parent:addChildLoot(childLoot)
end
return false
end

function MonsterType:createLoot(lootTable)
SortLootByChance(lootTable)
local lootError = false

for _, loot in pairs(lootTable) do
local parent = Loot()
if loot.name then
if not parent:setIdFromName(loot.name) then
lootError = true
end
if loot.child then
SortLootByChance(loot.child)
for _, children in pairs(loot.child) do
local child = Loot()
if children.name then
if not child:setIdFromName(children.name) then
lootError = true
end
else
if not isInteger(children.id) or children.id < 1 then
lootError = true
end
child:setId(children.id)
end
if children.subType or children.charges then
child:setSubType(children.subType or children.charges)
else
local cType = ItemType(children.name and children.name or children.id)
if cType and cType:getCharges() > 1 then
child:setSubType(cType:getCharges())
end
end
if children.chance then
child:setChance(children.chance)
end
if children.minCount then
child:setMinCount(children.minCount)
end
if children.maxCount then
child:setMaxCount(children.maxCount)
end
if children.aid or children.actionId then
child:setActionId(children.aid or children.actionId)
end
if children.text or children.description then
child:setText(children.text or children.description)
end
if loot.name then
child:setNameItem(loot.name)
end
if children.article then
child:setArticle(children.article)
end
if children.attack then
child:setAttack(children.attack)
end
if children.defense then
child:setDefense(children.defense)
end
if children.extraDefense or children.extraDef then
child:setExtraDefense(children.extraDefense or children.extraDef)
end
if children.armor then
child:setArmor(children.armor)
end
if children.shootRange or children.range then
child:setShootRange(children.shootRange or children.range)
end
if children.unique then
child:setUnique(children.unique)
end
parent:addChildLoot(child)
end
else
if not isInteger(loot.id) or loot.id < 1 then
lootError = true
end
mtype:addLoot(parent)
parent:setId(loot.id)
end
if lootError then
logger.warn("[registerMonsterType.loot] - Monster: {} loot could not correctly be load", mtype:name())

configureLootAttributes(parent, loot)

if loot.child and addChildrenLoot(parent, loot.child) then
lootError = true
end

self:addLoot(parent)
end

if lootError then
logger.warn("[MonsterType:createLoot] - Monster: {} loot could not be loaded correctly", self:getName())
end
end

registerMonsterType.loot = function(mtype, mask)
if type(mask.loot) == "table" then
mtype:createLoot(mask.loot)
end
end

local playerElements = { COMBAT_PHYSICALDAMAGE, COMBAT_ENERGYDAMAGE, COMBAT_EARTHDAMAGE, COMBAT_FIREDAMAGE, COMBAT_ICEDAMAGE, COMBAT_HOLYDAMAGE, COMBAT_DEATHDAMAGE }
registerMonsterType.elements = function(mtype, mask)
local min = configManager.getNumber(configKeys.MIN_ELEMENTAL_RESISTANCE)
Expand Down
74 changes: 74 additions & 0 deletions data/scripts/systems/custom_monster_loot.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- Drops custom loot for all monsters
local allLootConfig = {
--{ id = 6526, chance = 100000 }, -- Example of loot (100% chance)
}

-- Custom loot for specific monsters (this has the same usage options as normal monster loot)
local customLootConfig = {
["Dragon"] = { items = {
{ name = "platinum coin", chance = 1000, maxCount = 1 },
} },
}

-- Global loot addition for all monsters
local callback = EventCallback("MonsterOnDropLootCustom")

function callback.monsterOnDropLoot(monster, corpse)
if not monster or not corpse then
return
end
local player = Player(corpse:getCorpseOwner())
if not player or not player:canReceiveLoot() then
return
end
corpse:addLoot(monster:generateCustomLoot())
end

-- Register the callback only if there is loot to be dropped
if #allLootConfig > 0 then
callback:register()
end

function Monster:generateCustomLoot()
local mType = self:getType()
if not mType then
return {}
end

local loot = {}

for _, lootInfo in ipairs(allLootConfig) do
local roll = math.random(1, 10000)
if roll <= lootInfo.chance then
if loot[lootInfo.id] then
loot[lootInfo.id].count = loot[lootInfo.id].count + 1
else
loot[lootInfo.id] = { count = 1 }
end
end
end

return loot
end

local customMonsterLoot = GlobalEvent("CreateCustomMonsterLoot")

function customMonsterLoot.onStartup()
for monsterName, _ in pairs(customLootConfig) do
local mtype = Game.getMonsterTypeByName(monsterName)
if mtype then
local lootTable = customLootConfig[mtype:getName()]
if not lootTable then
logger.error("[customMonsterLoot.onStartup] - No custom loot found for monster: {}", self:getName())
return
end

if #lootTable.items > 0 then
mtype:createLoot(lootTable.items)
logger.debug("[customMonsterLoot.onStartup] - Custom loot registered for monster: {}", mtype:getName())
end
end
end
end

customMonsterLoot:register()
4 changes: 1 addition & 3 deletions src/creatures/monsters/monsters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,7 @@ void Monsters::clear() {
std::shared_ptr<MonsterType> Monsters::getMonsterType(const std::string &name, bool silent /* = false*/) const {
std::string lowerCaseName = asLowerCaseString(name);
if (auto it = monsters.find(lowerCaseName);
it != monsters.end()
// We will only return the MonsterType if it match the exact name of the monster
&& it->first.find(lowerCaseName) != std::basic_string<char>::npos) {
it != monsters.end()) {
return it->second;
}
if (!silent) {
Expand Down
2 changes: 0 additions & 2 deletions src/creatures/players/player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,6 @@ void Player::closeContainer(uint8_t cid) {
removeEmptyRewards();
}
openContainers.erase(it);
if (container && container->getID() == ITEM_BROWSEFIELD) {
}
}

void Player::removeEmptyRewards() {
Expand Down
2 changes: 1 addition & 1 deletion src/game/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ void Game::loadBoostedCreature() {
}

const auto oldRace = result->getNumber<uint16_t>("raceid");
const auto monsterlist = getBestiaryList();
const auto &monsterlist = getBestiaryList();

struct MonsterRace {
uint16_t raceId { 0 };
Expand Down
2 changes: 1 addition & 1 deletion src/io/iobestiary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ uint16_t IOBestiary::getBestiaryRaceUnlocked(const std::shared_ptr<Player> &play
}

uint16_t count = 0;
std::map<uint16_t, std::string> besty_l = g_game().getBestiaryList();
const std::map<uint16_t, std::string> &besty_l = g_game().getBestiaryList();

for (const auto &it : besty_l) {
const auto mtype = g_monsters().getMonsterType(it.second);
Expand Down
4 changes: 2 additions & 2 deletions src/io/ioprey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ void TaskHuntingSlot::reloadMonsterGrid(std::vector<uint16_t> blackList, uint32_
// Disabling task hunting system if the server have less then 36 registered monsters on bestiary because:
// - Impossible to generate random lists without duplications on slots.
// - Stress the server with unnecessary loops.
std::map<uint16_t, std::string> bestiary = g_game().getBestiaryList();
const std::map<uint16_t, std::string> &bestiary = g_game().getBestiaryList();
if (bestiary.size() < 36) {
return;
}
Expand Down Expand Up @@ -574,7 +574,7 @@ void IOPrey::initializeTaskHuntOptions() {
}

msg.addByte(0xBA);
std::map<uint16_t, std::string> bestiaryList = g_game().getBestiaryList();
const std::map<uint16_t, std::string> &bestiaryList = g_game().getBestiaryList();
msg.add<uint16_t>(static_cast<uint16_t>(bestiaryList.size()));
std::for_each(bestiaryList.begin(), bestiaryList.end(), [&msg](auto mType) {
const auto mtype = g_monsters().getMonsterType(mType.second);
Expand Down
18 changes: 18 additions & 0 deletions src/lua/functions/core/game/game_functions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ int GameFunctions::luaGameCreateNpcType(lua_State* L) {
return NpcTypeFunctions::luaNpcTypeCreate(L);
}

int GameFunctions::luaGameGetMonsterTypeByName(lua_State* L) {
if (!isString(L, 1)) {
reportErrorFunc("First argument must be a string");
return 1;
}

const auto name = getString(L, 1);
const auto &mType = g_monsters().getMonsterType(name);
if (!mType) {
reportErrorFunc(fmt::format("MonsterType with name {} not found", name));
return 1;
}

pushUserdata<MonsterType>(L, mType);
setMetatable(L, -1, "MonsterType");
return 1;
}

int GameFunctions::luaGameGetSpectators(lua_State* L) {
// Game.getSpectators(position[, multifloor = false[, onlyPlayer = false[, minRangeX = 0[, maxRangeX = 0[, minRangeY = 0[, maxRangeY = 0]]]]]])
const Position &position = getPosition(L, 1);
Expand Down
2 changes: 2 additions & 0 deletions src/lua/functions/core/game/game_functions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class GameFunctions final : LuaScriptInterface {

registerMethod(L, "Game", "createNpcType", GameFunctions::luaGameCreateNpcType);
registerMethod(L, "Game", "createMonsterType", GameFunctions::luaGameCreateMonsterType);
registerMethod(L, "Game", "getMonsterTypeByName", GameFunctions::luaGameGetMonsterTypeByName);

registerMethod(L, "Game", "getSpectators", GameFunctions::luaGameGetSpectators);

Expand Down Expand Up @@ -100,6 +101,7 @@ class GameFunctions final : LuaScriptInterface {
private:
static int luaGameCreateMonsterType(lua_State* L);
static int luaGameCreateNpcType(lua_State* L);
static int luaGameGetMonsterTypeByName(lua_State* L);

static int luaGameGetSpectators(lua_State* L);

Expand Down
Loading

0 comments on commit 34db7f7

Please sign in to comment.