Implement drops from cocoons/world bosses

This commit is contained in:
Melledy
2023-12-09 10:36:42 -08:00
parent 6af08f5034
commit 90d7bfee88
10 changed files with 354 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ public class GameConstants {
public static final int MAX_AVATARS_IN_TEAM = 4;
public static final int DEFAULT_TEAMS = 6;
public static final int MAX_MP = 5; // Client doesnt like more than 5
public static final int FARM_ELEMENT_STAMINA_COST = 30;
// Chat/Social
public static final int MAX_FRIENDSHIPS = 100;

View File

@@ -61,6 +61,7 @@ public class GameData {
private static Int2ObjectMap<EquipmentPromotionExcel> equipmentPromotionExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<MazeBuffExcel> mazeBuffExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<CocoonExcel> cocoonExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<MappingInfoExcel> mappingInfoExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<MonsterDropExcel> monsterDropExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<PlayerLevelExcel> playerLevelExcelMap = new Int2ObjectOpenHashMap<>();
@@ -219,6 +220,10 @@ public class GameData {
return cocoonExcelMap.get((cocoonId << 8) + worldLevel);
}
public static MappingInfoExcel getMappingInfoExcel(int mappingInfoId, int worldLevel) {
return mappingInfoExcelMap.get((mappingInfoId << 8) + worldLevel);
}
public static MonsterDropExcel getMonsterDropExcel(int monsterNpcId, int worldLevel) {
return monsterDropExcelMap.get((monsterNpcId << 4) + worldLevel);
}

View File

@@ -10,6 +10,7 @@ import lombok.Getter;
@ResourceType(name = {"CocoonConfig.json"})
public class CocoonExcel extends GameResource {
private int ID;
private int MappingInfoID;
private int WorldLevel;
private int PropID;
private int StaminaCost;

View File

@@ -0,0 +1,166 @@
package emu.lunarcore.data.excel;
import java.util.ArrayList;
import java.util.List;
import emu.lunarcore.GameConstants;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.data.common.ItemParam;
import emu.lunarcore.game.drops.DropParam;
import emu.lunarcore.game.enums.ItemMainType;
import emu.lunarcore.game.enums.ItemRarity;
import emu.lunarcore.game.enums.ItemSubType;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@ResourceType(name = {"MappingInfo.json"}, loadPriority = LoadPriority.LOW)
public class MappingInfoExcel extends GameResource {
private int ID;
private int WorldLevel;
private String FarmType; // is enum
private List<ItemParam> DisplayItemList;
private transient List<DropParam> dropList;
@Override
public int getId() {
return (ID << 8) + WorldLevel;
}
@Override
public void onLoad() {
// Temp way to pre-calculate drop list
this.dropList = new ArrayList<>(this.getDisplayItemList().size());
var equipmentDrops = new IntArrayList();
var relicDrops = new Int2ObjectOpenHashMap<IntList>();
for (var itemParam : this.getDisplayItemList()) {
// Add item param if the amount is already set in the excel
if (itemParam.getCount() > 0) {
dropList.add(new DropParam(itemParam.getId(), itemParam.getCount()));
continue;
}
// Multiplier. TODO drop rate is not correct
int multiplier = 1;
if (FarmType == null) {
// Skip
} else if (FarmType.equals("RELIC")) {
multiplier = 4;
} else if (FarmType.equals("COCOON2")) {
multiplier = 3;
} else if (FarmType.equals("ELEMENT")) {
multiplier = 3;
}
// Random credits
if (itemParam.getId() == GameConstants.MATERIAL_COIN_ID) {
// TODO drop rate is not correct
DropParam drop = new DropParam(itemParam.getId(), 0);
drop.setMinCount((50 + (this.getWorldLevel() * 10)) * multiplier);
drop.setMaxCount((100 + (this.getWorldLevel() * 10)) * multiplier);
dropList.add(drop);
continue;
}
// Get item excel
ItemExcel itemExcel = GameData.getItemExcelMap().get(itemParam.getId());
if (itemExcel == null) continue;
// Hacky way of calculating drops
if (itemExcel.getItemSubType() == ItemSubType.RelicSetShowOnly) {
// Get relic base id from relic display id
int baseRelicId = (itemParam.getId() / 10) % 1000;
int baseRarity = itemParam.getId() % 10;
// Add relics from the set
int relicStart = 20001 + (baseRarity * 10000) + (baseRelicId * 10);
int relicEnd = relicStart + 3;
for (;relicStart < relicEnd; relicStart++) {
ItemExcel relicExcel = GameData.getItemExcelMap().get(relicStart);
if (relicExcel == null) break;
relicDrops
.computeIfAbsent(baseRarity, r -> new IntArrayList())
.add(relicStart);
}
} else if (itemExcel.getItemMainType() == ItemMainType.Material) {
// Calculate amount to drop by purpose level
DropParam drop = switch (itemExcel.getPurposeType()) {
// Avatar exp. TODO drop rate is not correct
case 1 -> new DropParam(itemParam.getId(), 1);
// Boss materials
case 2 -> new DropParam(itemParam.getId(), this.getWorldLevel());
// Trace materials. TODO drop rate is not correct
case 3 -> {
var dropInfo = new DropParam(itemParam.getId(), 1);
if (itemExcel.getRarity() == ItemRarity.VeryRare) {
dropInfo.setChance((this.getWorldLevel() - 3) * 75);
}
yield dropInfo;
}
// Boss Trace materials. TODO drop rate is not correct
case 4 -> new DropParam(itemParam.getId(), (this.getWorldLevel() * 0.5) + 0.5);
// Lightcone exp. TODO drop rate is not correct
case 5 -> new DropParam(itemParam.getId(), 1);
// Lucent afterglow
case 11 -> new DropParam(itemParam.getId(), 4 + this.getWorldLevel());
// Unknown
default -> null;
};
if (drop != null) {
dropList.add(drop);
}
} else if (itemExcel.getItemMainType() == ItemMainType.Equipment) {
// Lightcones
equipmentDrops.add(itemParam.getId());
}
}
// Add equipment drops
if (equipmentDrops.size() > 0) {
DropParam drop = new DropParam();
drop.getItems().addAll(equipmentDrops);
drop.setCount(1);
drop.setChance((this.getWorldLevel() * 10) + 40);
dropList.add(drop);
}
// Add relic drops
if (relicDrops.size() > 0) {
for (var entry : relicDrops.int2ObjectEntrySet()) {
// Add items to drop param
DropParam drop = new DropParam();
drop.getItems().addAll(entry.getValue());
// Set count by rarity
double amount = switch (entry.getIntKey()) {
case 4:
yield (this.getWorldLevel() * 0.5) - 0.5;
case 3:
yield (this.getWorldLevel() * 0.5) + (this.getWorldLevel() == 2 ? 1.0 : 0);
case 2:
yield (6 - this.getWorldLevel()) + 0.5 - (this.getWorldLevel() == 1 ? 3.75 : 0);
default:
yield this.getWorldLevel() == 1 ? 6 : 2;
};
// Set amount
if (amount > 0) {
drop.setCount(amount);
dropList.add(drop);
}
}
}
}
}

View File

@@ -40,6 +40,11 @@ public class Battle {
@Setter private int levelOverride;
@Setter private int roundsLimit;
// Used for calculating cocoon/farm element drops
@Setter private int mappingInfoId;
@Setter private int worldLevel;
@Setter private int cocoonWave;
private Battle(Player player, PlayerLineup lineup) {
this.id = player.getNextBattleId();
this.player = player;

View File

@@ -119,8 +119,16 @@ public class BattleService extends BaseGameService {
// Add npc monsters
for (var monster : monsters) {
// Add npc monster
battle.getNpcMonsters().add(monster);
// Check farm element
if (monster.getFarmElementId() != 0) {
battle.setMappingInfoId(monster.getFarmElementId());
battle.setWorldLevel(monster.getWorldLevel());
battle.setStaminaCost(GameConstants.FARM_ELEMENT_STAMINA_COST);
}
// Handle monster buffs
// TODO handle multiple waves properly
monster.applyBuffs(battle);
@@ -213,6 +221,9 @@ public class BattleService extends BaseGameService {
// Build battle from cocoon data
Battle battle = new Battle(player, player.getLineupManager().getCurrentLineup(), stages);
battle.setMappingInfoId(cocoonExcel.getMappingInfoID());
battle.setCocoonWave(wave);
battle.setWorldLevel(worldLevel);
battle.setStaminaCost(cost);
player.setBattle(battle);
@@ -338,8 +349,18 @@ public class BattleService extends BaseGameService {
// Create new battle for player
Battle battle = new Battle(player, player.getCurrentLineup(), stage);
battle.setStaminaCost(GameConstants.FARM_ELEMENT_STAMINA_COST);
player.setBattle(battle);
// Get mapping info id
int mappingInfoId = ((stageId / 10) % 100) + 1100;
int mappingInfoLevel = stageId % 10;
var mappingInfoExcel = GameData.getMappingInfoExcel(mappingInfoId, mappingInfoLevel);
if (mappingInfoExcel != null && mappingInfoExcel.getFarmType() != null && mappingInfoExcel.getFarmType().equals("ELEMENT")) {
battle.setMappingInfoId(mappingInfoId);
battle.setWorldLevel(mappingInfoLevel);
}
// Send packet
player.sendPacket(new PacketReEnterLastElementStageScRsp(battle));
}

View File

@@ -0,0 +1,11 @@
package emu.lunarcore.game.drops;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
public class DropMap extends Int2IntOpenHashMap {
private static final long serialVersionUID = -4186524272780523459L;
public FastEntrySet entries() {
return this.int2IntEntrySet();
}
}

View File

@@ -0,0 +1,99 @@
package emu.lunarcore.game.drops;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.excel.ItemExcel;
import emu.lunarcore.util.Utils;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class DropParam {
@Setter(AccessLevel.NONE)
private IntList items;
private int minCount;
private int maxCount;
private int chance;
public DropParam() {
this.items = new IntArrayList();
this.chance = 1000;
}
public DropParam(int itemId, int count) {
this();
this.getItems().add(itemId);
this.setCount(count);
}
public DropParam(int itemId, double count) {
this();
this.getItems().add(itemId);
this.setCount(count);
}
public void setCount(int count) {
this.minCount = count;
this.maxCount = count;
}
public void setCount(double count) {
if (count % 1 == 0) {
this.setCount((int) count);
} else {
this.setMaxCount((int) Math.ceil(count));
this.setMinCount((int) Math.floor(count));
}
}
public int generateItemId() {
if (this.items == null || this.items.size() == 0) {
return 0;
}
if (this.items.size() == 1) {
return this.items.getInt(0);
}
return Utils.randomElement(this.items);
}
public int generateCount() {
if (this.maxCount > this.minCount) {
return Utils.randomRange(this.minCount, this.maxCount);
}
return this.maxCount;
}
public void roll(DropMap drops) {
// Check drop chance
if (this.chance < 1000) {
int random = Utils.randomRange(0, 999);
if (random > this.chance) {
return;
}
}
// Get count
int count = generateCount();
// Generate item(s)
while (count > 0) {
int itemId = generateItemId();
ItemExcel excel = GameData.getItemExcelMap().get(itemId);
if (excel == null) break;
if (excel.isEquippable()) {
drops.addTo(itemId, 1);
count--;
} else {
drops.addTo(itemId, count);
count -= count;
}
}
}
}

View File

@@ -6,13 +6,13 @@ import java.util.List;
import emu.lunarcore.GameConstants;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.common.ItemParam;
import emu.lunarcore.data.excel.ItemExcel;
import emu.lunarcore.game.battle.Battle;
import emu.lunarcore.game.inventory.GameItem;
import emu.lunarcore.game.scene.entity.EntityMonster;
import emu.lunarcore.server.game.BaseGameService;
import emu.lunarcore.server.game.GameServer;
import emu.lunarcore.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
public class DropService extends BaseGameService {
@@ -20,15 +20,12 @@ public class DropService extends BaseGameService {
super(server);
}
// TODO this isnt the right way drops are calculated on the official server... but its good enough for now
public void calculateDrops(Battle battle) {
// TODO this isnt the right way drops are calculated on the official server... but its good enough for now
if (battle.getNpcMonsters().size() == 0) {
return;
}
// Setup drop map
var dropMap = new DropMap();
var dropMap = new Int2IntOpenHashMap();
// Get drops from monsters
// Calculate drops from monsters
for (EntityMonster monster : battle.getNpcMonsters()) {
var dropExcel = GameData.getMonsterDropExcel(monster.getExcel().getId(), monster.getWorldLevel());
if (dropExcel == null || dropExcel.getDisplayItemList() == null) {
@@ -43,22 +40,50 @@ public class DropService extends BaseGameService {
count = dropExcel.getAvatarExpReward();
}
dropMap.put(id, count + dropMap.get(id));
dropMap.addTo(id, count);
}
}
for (var entry : dropMap.int2IntEntrySet()) {
// Mapping info
if (battle.getMappingInfoId() > 0) {
var mapInfoExcel = GameData.getMappingInfoExcel(battle.getMappingInfoId(), battle.getWorldLevel());
if (mapInfoExcel != null) {
int rolls = Math.max(battle.getCocoonWave(), 1);
for (var dropParam : mapInfoExcel.getDropList()) {
for (int i = 0; i < rolls; i++) {
dropParam.roll(dropMap);
}
}
}
}
// Sanity check
if (dropMap.size() == 0) {
return;
}
// Create drops
for (var entry : dropMap.entries()) {
if (entry.getIntValue() <= 0) {
continue;
}
// Create item and add it to player
GameItem item = new GameItem(entry.getIntKey(), entry.getIntValue());
ItemExcel excel = GameData.getItemExcelMap().get(entry.getIntKey());
if (excel == null) continue;
if (battle.getPlayer().getInventory().addItem(item)) {
battle.getDrops().add(item);
// Add item
if (excel.isEquippable()) {
for (int i = 0; i < entry.getIntValue(); i++) {
battle.getDrops().add(new GameItem(excel, 1));
}
} else {
battle.getDrops().add(new GameItem(excel, entry.getIntValue()));
}
}
// Add to inventory
battle.getPlayer().getInventory().addItems(battle.getDrops());
}
// TODO filler

View File

@@ -8,6 +8,8 @@ import java.util.Base64;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import it.unimi.dsi.fastutil.ints.IntList;
public class Utils {
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
@@ -176,6 +178,10 @@ public class Utils {
return list.get(ThreadLocalRandom.current().nextInt(0, list.size()));
}
public static int randomElement(IntList list) {
return list.getInt(ThreadLocalRandom.current().nextInt(0, list.size()));
}
/**
* Checks if an integer array contains a value
* @param array