Files
Server/src/engine/gameManager/LootManager.java
T
MagicBotandClaude Opus 5.5 6b4e9815e0 Switch Item.template and MobLoot to CObjectTemplate
Item.template is now the cache template from CObjectTemplate.lookup
instead of the legacy DB ItemTemplate, so item.template.cacheTemplate.X
becomes item.template.X. MobLoot, LootManager, ItemManager.isTwoHanded/
isShield and CombatManager.getSwingAnimation take a CObjectTemplate.

No value changes: every getter already read the cache template through
the bridge, and the lookup resolves the same 7586 ids as
static_item_templates with the same item_type (offline parity p17.py,
0 differences).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:55:28 -04:00

468 lines
17 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.loot.*;
import engine.mbEnums;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.chat.ChatSystemMsg;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ThreadLocalRandom;
/**
* Class contains static methods for data from Magicbane's loot tables
*/
public enum LootManager {
// MB Dev notes:
// Class implements all loot generation mechanics for Magicbane.
// Class handles variables like drop rates
// Generation initiates in GenerateMobLoot and is passed a mobile
// Utilized database tables:
// - static_loot_bootySet - static_loot_gen - static_loot_item - static_loot_mod - static_loot_modtype
// Class also handles mechanic for Fate Peddler
//
LOOTMANAGER;
// Newer tables
public static HashMap<Integer, ArrayList<GenTableEntry>> _genTables = new HashMap<>();
public static HashMap<Integer, ArrayList<ItemTableEntry>> _itemTables = new HashMap<>();
public static HashMap<Integer, ArrayList<ModTableEntry>> _modTables = new HashMap<>();
public static HashMap<Integer, ArrayList<ModTypeTableEntry>> _modTypeTables = new HashMap<>();
// Drop Rates
public static float NORMAL_DROP_RATE;
public static float NORMAL_EXP_RATE;
public static float NORMAL_GOLD_RATE;
public static float HOTZONE_DROP_RATE;
public static float HOTZONE_EXP_RATE;
public static float HOTZONE_GOLD_RATE;
public static HashMap<Integer, ArrayList<BootySetEntry>> _bootySetMap = new HashMap<>();
// Bootstrap routine to initialize the Loot Manager
public static void init() {
// Load loot tables from database.
_genTables = DbManager.LootQueries.LOAD_GEN_ITEM_TABLES();
_itemTables = DbManager.LootQueries.LOAD_ITEM_TABLES();
_modTables = DbManager.LootQueries.LOAD_MOD_TABLES();
_modTypeTables = DbManager.LootQueries.LOAD_MOD_TYPE_TABLES();
// Cache drop rate values from Config manager.
NORMAL_DROP_RATE = Float.parseFloat(ConfigManager.MB_NORMAL_DROP_RATE.getValue());
NORMAL_EXP_RATE = Float.parseFloat(ConfigManager.MB_NORMAL_EXP_RATE.getValue());
NORMAL_GOLD_RATE = Float.parseFloat(ConfigManager.MB_NORMAL_GOLD_RATE.getValue());
HOTZONE_DROP_RATE = Float.parseFloat(ConfigManager.MB_HOTZONE_DROP_RATE.getValue());
HOTZONE_EXP_RATE = Float.parseFloat(ConfigManager.MB_HOTZONE_EXP_RATE.getValue());
HOTZONE_GOLD_RATE = Float.parseFloat(ConfigManager.MB_HOTZONE_GOLD_RATE.getValue());
}
public static void GenerateMobLoot(Mob mob) {
//determine if mob is in hotzone
boolean inHotzone = ZoneManager.inHotZone(mob.getLoc());
//iterate the booty sets
if (mob.getMobBase().bootySet != 0 && _bootySetMap.containsKey(mob.getMobBase().bootySet) == true)
RunBootySet(_bootySetMap.get(mob.getMobBase().bootySet), mob, inHotzone);
if (mob.bootySet != 0 && _bootySetMap.containsKey(mob.bootySet) == true)
RunBootySet(_bootySetMap.get(mob.bootySet), mob, inHotzone);
//lastly, check mobs inventory for godly or disc runes to send a server announcement
for (Item item : mob.getInventory()) {
if ((item.templateID > 2499 && item.templateID <= 3050) || item.template.getItemBaseName().toLowerCase().contains("of the gods")) {
ChatSystemMsg chatMsg = new ChatSystemMsg(null, mob.getName() + " in " + mob.getParentZone().zoneName + " has found the " + item.template.getItemBaseName() + ". Are you tough enough to take it?");
chatMsg.setMessageType(10);
chatMsg.setChannel(mbEnums.ChatChannelType.SYSTEM.getChannelID());
DispatchManager.dispatchMsgToAll(chatMsg);
}
}
}
private static void RunBootySet(ArrayList<BootySetEntry> entries, Mob mob, boolean inHotzone) {
boolean hotzoneWasRan = false;
float dropRate = 1.0f;
// Iterate all entries in this bootySet and process accordingly
for (BootySetEntry bse : entries) {
switch (bse.bootyType) {
case "GOLD":
GenerateGoldDrop(mob, bse, inHotzone);
break;
case "LOOT":
if (mob.getSafeZone() == false)
dropRate = LootManager.NORMAL_DROP_RATE;
if (inHotzone == true)
dropRate = LootManager.HOTZONE_DROP_RATE;
if (ThreadLocalRandom.current().nextInt(1, 100 + 1) < (bse.dropChance * dropRate))
GenerateLootDrop(mob, bse.genTable, false); //generate normal loot drop
// Generate hotzone loot if in hotzone
// Only one bite at the hotzone apple per bootyset.
if (inHotzone == true && hotzoneWasRan == false)
if (_genTables.containsKey(bse.genTable + 1) && ThreadLocalRandom.current().nextInt(1, 100 + 1) < (bse.dropChance * dropRate)) {
GenerateLootDrop(mob, bse.genTable + 1, true); //generate loot drop from hotzone table
hotzoneWasRan = true;
}
break;
case "ITEM":
GenerateInventoryDrop(mob, bse);
break;
}
}
}
public static MobLoot getGenTableItem(int genTableID, AbstractCharacter mob, Boolean inHotzone) {
if (mob == null || _genTables.containsKey(genTableID) == false)
return null;
MobLoot outItem;
int genRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1);
GenTableEntry selectedRow = GenTableEntry.rollTable(genTableID, genRoll, 1.0f);
if (selectedRow == null)
return null;
int itemTableId = selectedRow.itemTableID;
if (_itemTables.containsKey(itemTableId) == false)
return null;
//gets the 1-320 roll for this mob
int itemTableRoll = 0;
if (mob.getObjectType().ordinal() == 52) //52 = player character
itemTableRoll = ThreadLocalRandom.current().nextInt(1, 320 + 1);
else
itemTableRoll = TableRoll(mob.level, inHotzone);
ItemTableEntry tableRow = ItemTableEntry.rollTable(itemTableId, itemTableRoll);
if (tableRow == null)
return null;
int itemUUID = tableRow.templateID;
if (itemUUID == 0)
return null;
if (CObjectTemplate.lookup(itemUUID).getItemType().equals(mbEnums.ItemType.RESOURCE)) {
int amount = ThreadLocalRandom.current().nextInt(tableRow.minSpawn, tableRow.maxSpawn + 1);
return new MobLoot(mob, CObjectTemplate.lookup(itemUUID), amount, false);
}
outItem = new MobLoot(mob, CObjectTemplate.lookup(itemUUID), false);
if (selectedRow.pModTable != 0)
try {
outItem = GeneratePrefix(mob, outItem, genTableID, genRoll, inHotzone);
outItem.flags.remove(mbEnums.ItemFlags.Identified);
} catch (Exception e) {
Logger.error("Failed to GeneratePrefix for item: " + outItem.getName());
}
if (selectedRow.sModTable != 0)
try {
outItem = GenerateSuffix(mob, outItem, genTableID, genRoll, inHotzone);
outItem.flags.remove(mbEnums.ItemFlags.Identified);
} catch (Exception e) {
Logger.error("Failed to GenerateSuffix for item: " + outItem.getName());
}
return outItem;
}
private static MobLoot GeneratePrefix(AbstractCharacter mob, MobLoot inItem, int genTableID, int genRoll, Boolean inHotzone) {
GenTableEntry selectedRow = GenTableEntry.rollTable(genTableID, genRoll, 1.0f);
if (selectedRow == null)
return inItem;
int prefixroll = ThreadLocalRandom.current().nextInt(1, 100 + 1);
ModTypeTableEntry prefixTable = ModTypeTableEntry.rollTable(selectedRow.pModTable, prefixroll);
if (prefixTable == null)
return inItem;
int prefixTableRoll = 0;
if (mob.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter))
prefixTableRoll = ThreadLocalRandom.current().nextInt(1, 320 + 1);
else
prefixTableRoll = TableRoll(mob.level, inHotzone);
ModTableEntry prefixMod = ModTableEntry.rollTable(prefixTable.modTableID, prefixTableRoll);
if (prefixMod == null)
return inItem;
if (prefixMod.action.isEmpty() == false) {
inItem.setPrefix(prefixMod.action);
inItem.addPermanentEnchantment(prefixMod.action, 0, prefixMod.level, true);
}
return inItem;
}
private static MobLoot GenerateSuffix(AbstractCharacter mob, MobLoot inItem, int genTableID, int genRoll, Boolean inHotzone) {
GenTableEntry selectedRow = GenTableEntry.rollTable(genTableID, genRoll, 1.0f);
if (selectedRow == null)
return inItem;
int suffixRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1);
ModTypeTableEntry suffixTable = ModTypeTableEntry.rollTable(selectedRow.sModTable, suffixRoll);
if (suffixTable == null)
return inItem;
int suffixTableRoll;
if (mob.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter))
suffixTableRoll = ThreadLocalRandom.current().nextInt(1, 320 + 1);
else
suffixTableRoll = TableRoll(mob.level, inHotzone);
ModTableEntry suffixMod = ModTableEntry.rollTable(suffixTable.modTableID, suffixTableRoll);
if (suffixMod == null)
return inItem;
if (suffixMod.action.isEmpty() == false) {
inItem.setSuffix(suffixMod.action);
inItem.addPermanentEnchantment(suffixMod.action, 0, suffixMod.level, false);
}
return inItem;
}
public static int TableRoll(int mobLevel, Boolean inHotzone) {
if (mobLevel > 65)
mobLevel = 65;
int max = (int) (4.882 * mobLevel + 127.0);
int min = (int) (4.469 * mobLevel - 3.469);
min = Math.max(min, 70);
max = Math.min(max, 319);
if (inHotzone)
min += mobLevel;
int roll = ThreadLocalRandom.current().nextInt(min, max + 1);
return roll;
}
public static void GenerateGoldDrop(Mob mob, BootySetEntry bse, Boolean inHotzone) {
int chanceRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1);
//early exit, failed to hit minimum chance roll
if (chanceRoll > bse.dropChance)
return;
//determine and add gold to mob inventory
int high = bse.highGold;
int low = bse.lowGold;
int gold = ThreadLocalRandom.current().nextInt(low, high + 1);
if (inHotzone == true)
gold = (int) (gold * HOTZONE_GOLD_RATE);
else
gold = (int) (gold * NORMAL_GOLD_RATE);
if (gold > 0) {
MobLoot goldAmount = new MobLoot(mob, gold);
mob.charItemManager.addItemToInventory(goldAmount);
}
}
public static void GenerateLootDrop(Mob mob, int tableID, Boolean inHotzone) {
try {
MobLoot toAdd = getGenTableItem(tableID, mob, inHotzone);
if (toAdd != null)
mob.charItemManager.addItemToInventory(toAdd);
} catch (Exception e) {
//TODO chase down loot generation error, affects roughly 2% of drops
int i = 0;
}
}
public static void GenerateEquipmentDrop(Mob mob) {
if (mob.behaviourType.equals(mbEnums.MobBehaviourType.HamletGuard))
return; // safehold guards don't drop their equipment
//do equipment here
int dropCount = 0;
if (mob.charItemManager.equipped.isEmpty() == false)
for (Item item : mob.charItemManager.equipped.values()) {
if (item.drop_chance == 0)
continue;
float equipmentRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1);
float dropChance = item.drop_chance * 100;
if (equipmentRoll > dropChance)
continue;
MobLoot ml = new MobLoot(mob, item.template, false);
if (ml != null && dropCount < 1) {
ml.flags.add(mbEnums.ItemFlags.Identified);
ml.setCombat_health_current((short) ((short) ml.combat_health_current - ThreadLocalRandom.current().nextInt(5) + 1));
mob.charItemManager.addItemToInventory(ml);
dropCount = 1;
//break; // Exit on first successful roll.
}
}
}
public static void GenerateInventoryDrop(Mob mob, BootySetEntry bse) {
int chanceRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1);
//early exit, failed to hit minimum chance roll
if (chanceRoll > bse.dropChance)
return;
MobLoot lootItem = new MobLoot(mob, CObjectTemplate.lookup(bse.templateID), true);
if (lootItem != null)
mob.charItemManager.addItemToInventory(lootItem);
}
public static void peddleFate(PlayerCharacter playerCharacter, Item gift) {
//get table ID for the itemtemplate
int tableID = 0;
if (_bootySetMap.get(gift.templateID) != null)
tableID = _bootySetMap.get(gift.templateID).get(ThreadLocalRandom.current().nextInt(_bootySetMap.get(gift.templateID).size())).genTable;
if (tableID == 0)
return;
//get the character item manager
CharacterItemManager itemMan = playerCharacter.charItemManager;
if (itemMan == null)
return;
//check if player owns the gift he is trying to open
if (itemMan.doesCharOwnThisItem(gift.getObjectUUID()) == false)
return;
//roll 1-100 for the gen table selection
int genRoll = ThreadLocalRandom.current().nextInt(1, 100 + 1);
GenTableEntry selectedRow = GenTableEntry.rollTable(tableID, genRoll, LootManager.NORMAL_DROP_RATE);
if (selectedRow == null)
return;
//roll 220-320 for the item table selection
int itemRoll = ThreadLocalRandom.current().nextInt(220, 320 + 1);
ItemTableEntry selectedItem = ItemTableEntry.rollTable(selectedRow.itemTableID, itemRoll);
if (selectedItem == null)
return;
//create the item from the table, quantity is always 1
MobLoot winnings = new MobLoot(playerCharacter, CObjectTemplate.lookup(selectedItem.templateID), 1, false);
if (winnings == null)
return;
//early exit if the inventory of the player will not old the item
if (itemMan.hasRoomInventory(winnings.template.getItemWt()) == false) {
ErrorPopupMsg.sendErrorPopup(playerCharacter, 21);
return;
}
//determine if the winning item needs a prefix
if (selectedRow.pModTable != 0) {
int prefixRoll = ThreadLocalRandom.current().nextInt(220, 320 + 1);
ModTableEntry prefix = ModTableEntry.rollTable(selectedRow.pModTable, prefixRoll);
if (prefix != null)
winnings.addPermanentEnchantment(prefix.action, 0, prefix.level, true);
}
//determine if the winning item needs a suffix
if (selectedRow.sModTable != 0) {
int suffixRoll = ThreadLocalRandom.current().nextInt(220, 320 + 1);
ModTableEntry suffix = ModTableEntry.rollTable(selectedRow.sModTable, suffixRoll);
if (suffix != null)
winnings.addPermanentEnchantment(suffix.action, 0, suffix.level, true);
}
winnings.flags.add(mbEnums.ItemFlags.Identified);
//remove gift from inventory
itemMan.consume(gift);
//add winnings to player inventory
Item playerWinnings = winnings.promoteToItem(playerCharacter);
itemMan.addItemToInventory(playerWinnings);
itemMan.updateInventory();
}
}