diff --git a/.gitignore b/.gitignore index daafb05..72caa1b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ build data run libs/PartyPro + +.vscode diff --git a/src/main/java/com/azuredoom/levelingcore/LevelingCore.java b/src/main/java/com/azuredoom/levelingcore/LevelingCore.java index dce7b6c..ed5a42d 100644 --- a/src/main/java/com/azuredoom/levelingcore/LevelingCore.java +++ b/src/main/java/com/azuredoom/levelingcore/LevelingCore.java @@ -38,6 +38,7 @@ import com.azuredoom.levelingcore.level.mobs.mapping.MobBiomeMapping; import com.azuredoom.levelingcore.level.mobs.mapping.MobEnvironmentMapping; import com.azuredoom.levelingcore.level.mobs.mapping.MobInstanceMapping; +import com.azuredoom.levelingcore.level.mobs.mapping.MobOverrideMapping; import com.azuredoom.levelingcore.level.mobs.mapping.MobZoneMapping; import com.azuredoom.levelingcore.level.rewards.LevelRewards; import com.azuredoom.levelingcore.level.rewards.RewardEntry; @@ -99,6 +100,10 @@ public class LevelingCore extends JavaPlugin { LevelingCore.configPath ); + public static final Map mobOverrideMapping = MobOverrideMapping.loadOrCreate( + LevelingCore.configPath + ); + public static final MobLevelRegistry mobLevelRegistry = new MobLevelRegistry(); public static final MobLevelPersistence mobLevelPersistence = new MobLevelPersistence(); diff --git a/src/main/java/com/azuredoom/levelingcore/config/GUIConfig.java b/src/main/java/com/azuredoom/levelingcore/config/GUIConfig.java index 3daa358..69ea69b 100644 --- a/src/main/java/com/azuredoom/levelingcore/config/GUIConfig.java +++ b/src/main/java/com/azuredoom/levelingcore/config/GUIConfig.java @@ -235,6 +235,12 @@ public class GUIConfig { (exConfig, extraInfo) -> exConfig.levelMode ) .add() + .append( + new KeyedCodec("LevelVariance", Codec.INTEGER), + (exConfig, anInteger, extraInfo) -> exConfig.levelVariance = anInteger, + (exConfig, extraInfo) -> exConfig.levelVariance + ) + .add() .append( new KeyedCodec("MobHealthMultiplier", Codec.FLOAT), (exConfig, aFloat, extraInfo) -> exConfig.mobHealthMultiplier = aFloat, @@ -377,6 +383,8 @@ public class GUIConfig { private String levelMode = "NEARBY_PLAYERS_MEAN"; + private int levelVariance = 0; + private float mobHealthMultiplier = 2.10F; private float mobDamageMultiplier = 0.25F; @@ -624,6 +632,15 @@ public String getLevelMode() { return levelMode; } + /** + * Retrieves the configured level variance used by the leveling system. + * + * @return the level variance as an integer. + */ + public int getLevelVariance() { + return levelVariance; + } + public float getMobHealthMultiplier() { return mobHealthMultiplier; } diff --git a/src/main/java/com/azuredoom/levelingcore/config/internal/ConfigBootstrap.java b/src/main/java/com/azuredoom/levelingcore/config/internal/ConfigBootstrap.java index 79fa656..5d66ad8 100644 --- a/src/main/java/com/azuredoom/levelingcore/config/internal/ConfigBootstrap.java +++ b/src/main/java/com/azuredoom/levelingcore/config/internal/ConfigBootstrap.java @@ -38,6 +38,7 @@ public record Bootstrap( Map mobZoneMapping, Map mobBiomeMapping, Map mobEnvironmentMapping, + Map mobOverrideMapping, AutoCloseable closeable ) {} @@ -76,6 +77,7 @@ public static Bootstrap bootstrap(Path dataDir) { var mobZoneMapping = LevelingCore.mobZoneMapping; var mobBiomeMapping = LevelingCore.mobBiomeMapping; var mobEnvironmentMapping = LevelingCore.mobEnvironmentMapping; + var mobOverrideMapping = LevelingCore.mobOverrideMapping; return new Bootstrap( service, @@ -87,6 +89,7 @@ public static Bootstrap bootstrap(Path dataDir) { mobZoneMapping, mobBiomeMapping, mobEnvironmentMapping, + mobOverrideMapping, repo::close ); } diff --git a/src/main/java/com/azuredoom/levelingcore/level/mobs/mapping/MobOverrideMapping.java b/src/main/java/com/azuredoom/levelingcore/level/mobs/mapping/MobOverrideMapping.java new file mode 100644 index 0000000..8b1ee6a --- /dev/null +++ b/src/main/java/com/azuredoom/levelingcore/level/mobs/mapping/MobOverrideMapping.java @@ -0,0 +1,108 @@ +package com.azuredoom.levelingcore.level.mobs.mapping; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; + +import com.azuredoom.levelingcore.LevelingCore; +import com.azuredoom.levelingcore.config.internal.ConfigManager; +import com.azuredoom.levelingcore.exceptions.LevelingCoreException; + +public class MobOverrideMapping { + + public static final String FILE_NAME = "moboverridemapping.csv"; + + public static final String RESOURCE_DEFAULT = "/defaultmoboverridemapping.csv"; + + private MobOverrideMapping() {} + + public static Map loadOrCreate(Path dataDir) { + try { + Files.createDirectories(dataDir); + var configPath = dataDir.resolve(FILE_NAME); + + if (Files.notExists(configPath)) { + try (InputStream in = ConfigManager.class.getResourceAsStream(RESOURCE_DEFAULT)) { + if (in == null) { + throw new LevelingCoreException( + "defaultmoboverridemapping.csv not found in resources (expected at " + RESOURCE_DEFAULT + + ")" + ); + } + LevelingCore.LOGGER.at(Level.INFO) + .log("Creating default Mob Override Levels Mapping config at " + configPath); + Files.copy(in, configPath, StandardCopyOption.REPLACE_EXISTING); + } + } + + var mapping = readXpCsv(configPath); + + LevelingCore.LOGGER.at(Level.INFO) + .log( + "Loaded Mob Override Levels Mapping mapping from " + configPath + " " + mapping.size() + " entries)" + ); + return mapping; + + } catch (Exception e) { + throw new LevelingCoreException("Failed to load Mob Override Levels Mapping config", e); + } + } + + private static Map readXpCsv(Path csvPath) throws Exception { + Map out = new LinkedHashMap<>(); + + try (var reader = Files.newBufferedReader(csvPath, StandardCharsets.UTF_8)) { + String line; + var firstNonEmptyLine = true; + + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) + continue; + if (line.startsWith("#")) + continue; + + if (firstNonEmptyLine) { + firstNonEmptyLine = false; + if (line.equalsIgnoreCase("npc_id,lvl")) { + continue; + } + } + + var parts = line.split(",", 2); + if (parts.length != 2) { + LevelingCore.LOGGER.at(Level.WARNING).log("Skipping invalid CSV line: " + line); + continue; + } + + var npcId = parts[0].trim(); + var lvlStr = parts[1].trim(); + + if (npcId.isEmpty()) { + LevelingCore.LOGGER.at(Level.WARNING).log("Skipping CSV line with empty NPC ID: " + line); + continue; + } + + int lvl; + try { + lvl = Integer.parseInt(lvlStr); + } catch (NumberFormatException nfe) { + LevelingCore.LOGGER.at(Level.WARNING) + .log( + "Invalid NPC ID value for " + npcId + ": " + lvlStr + " (line: " + line + ")" + ); + continue; + } + + out.put(npcId, lvl); + } + } + + return out; + } +} diff --git a/src/main/java/com/azuredoom/levelingcore/utils/MobLevelingUtil.java b/src/main/java/com/azuredoom/levelingcore/utils/MobLevelingUtil.java index a74bbcf..3e609a2 100644 --- a/src/main/java/com/azuredoom/levelingcore/utils/MobLevelingUtil.java +++ b/src/main/java/com/azuredoom/levelingcore/utils/MobLevelingUtil.java @@ -35,30 +35,29 @@ public static int computeDynamicLevel( Store store ) { var modeStr = config.get().getLevelMode(); + var overrideLevel = computeNPCOverrideLevel(npc); + + if (overrideLevel != 0) { + return overrideLevel; + } if (modeStr == null) { - return computeNearbyPlayersMeanLevel(transform, store); + return computeNearbyPlayersMeanLevel(transform, store, npc); } return CoreLevelMode.fromString(modeStr) .map(mode -> switch (mode) { - case SPAWN_ONLY -> - computeSpawnLevel(npc); - case NEARBY_PLAYERS_MEAN -> - computeNearbyPlayersMeanLevel(transform, store); - case BIOME -> - computeBiomeLevel(store); - case ZONE -> - computeZoneLevel(store); - case ENVIRONMENT -> - computeEnvironmentLevel(transform, store); - case INSTANCE -> - computeInstanceLevel(store); + case SPAWN_ONLY -> computeSpawnLevel(npc); + case NEARBY_PLAYERS_MEAN -> computeNearbyPlayersMeanLevel(transform, store, npc); + case BIOME -> computeBiomeLevel(store, npc); + case ZONE -> computeZoneLevel(store, npc); + case ENVIRONMENT -> computeEnvironmentLevel(transform, store, npc); + case INSTANCE -> computeInstanceLevel(store, npc); }) .orElseGet(() -> { LevelingCore.LOGGER.at(Level.INFO) .log("Unknown level mode " + modeStr + " defaulting to NEARBY_PLAYERS_MEAN"); - return computeNearbyPlayersMeanLevel(transform, store); + return computeNearbyPlayersMeanLevel(transform, store, npc); }); } @@ -97,7 +96,7 @@ public static int computeSpawnLevel(NPCEntity npc) { return spawnMin + rng.nextInt((spawnMax - spawnMin) + 1); } - public static int computeInstanceLevel(Store store) { + public static int computeInstanceLevel(Store store, NPCEntity npc) { var world = store.getExternalData().getWorld(); var instanceName = world.getName(); var instanceMapping = LevelingCore.mobInstanceMapping; @@ -107,10 +106,11 @@ public static int computeInstanceLevel(Store store) { return 0; } - return instanceMapping.getOrDefault(instanceName.toLowerCase(), 1); + var baseLevel = instanceMapping.getOrDefault(instanceName.toLowerCase(), 1); + return randomizeLevel(baseLevel, npc); } - public static int computeZoneLevel(Store store) { + public static int computeZoneLevel(Store store, NPCEntity npc) { var world = store.getExternalData().getWorld(); var worldMapTracker = world.getPlayers().getFirst().getWorldMapTracker(); var currentZone = worldMapTracker.getCurrentZone(); @@ -118,10 +118,11 @@ public static int computeZoneLevel(Store store) { return 0; var zoneMapping = LevelingCore.mobZoneMapping; - return zoneMapping.getOrDefault(currentZone.zoneName().toLowerCase(), 1); + var baseLevel = zoneMapping.getOrDefault(currentZone.zoneName().toLowerCase(), 1); + return randomizeLevel(baseLevel, npc); } - public static int computeBiomeLevel(Store store) { + public static int computeBiomeLevel(Store store, NPCEntity npc) { var world = store.getExternalData().getWorld(); var worldMapTracker = world.getPlayers().getFirst().getWorldMapTracker(); var currentBiome = worldMapTracker.getCurrentBiomeName(); @@ -130,11 +131,11 @@ public static int computeBiomeLevel(Store store) { return 6; var biomeMapping = LevelingCore.mobBiomeMapping; - - return biomeMapping.getOrDefault(currentBiome.toLowerCase(), 1); + var baseLevel = biomeMapping.getOrDefault(currentBiome.toLowerCase(), 1); + return randomizeLevel(baseLevel, npc); } - public static int computeEnvironmentLevel(TransformComponent transform, Store store) { + public static int computeEnvironmentLevel(TransformComponent transform, Store store, NPCEntity npc) { var world = store.getExternalData().getWorld(); var mobPos = transform.getPosition(); var chunk = world.getChunk(ChunkUtil.indexChunkFromBlock((int) mobPos.x, (int) mobPos.z)); @@ -160,11 +161,15 @@ public static int computeEnvironmentLevel(TransformComponent transform, Store store) { + public static int computeNearbyPlayersMeanLevel( + TransformComponent transform, + Store store, + NPCEntity npc + ) { var world = store.getExternalData().getWorld(); var mobPos = transform.getPosition(); var players = world.getPlayers(); @@ -191,6 +196,26 @@ public static int computeNearbyPlayersMeanLevel(TransformComponent transform, St return 5; var mean = (double) sum / (double) count; - return (int) Math.round(mean); + var baseLevel = (int) Math.round(mean); + return randomizeLevel(baseLevel, npc); + } + + public static int computeNPCOverrideLevel(NPCEntity npc) { + var npcTypeID = npc.getNPCTypeId(); + var overrideMapping = LevelingCore.mobOverrideMapping; + + return overrideMapping.getOrDefault(npcTypeID.toLowerCase(), 0); + } + + public static int randomizeLevel(int baseLevel, NPCEntity npc) { + var variance = LevelingCore.getConfig().get().getLevelVariance(); + if (variance <= 0) { + return baseLevel; + } + var seed = npc.getUuid().getMostSignificantBits() ^ npc.getUuid().getLeastSignificantBits(); + var rng = new Random(seed); + + var range = baseLevel + variance; + return baseLevel + rng.nextInt(range - baseLevel); } } diff --git a/src/main/resources/defaultmoboverridemapping.csv b/src/main/resources/defaultmoboverridemapping.csv new file mode 100644 index 0000000..91c8ab9 --- /dev/null +++ b/src/main/resources/defaultmoboverridemapping.csv @@ -0,0 +1,2 @@ +npc_id,lvl +bunny,5 \ No newline at end of file