Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ build
data
run
libs/PartyPro

.vscode
5 changes: 5 additions & 0 deletions src/main/java/com/azuredoom/levelingcore/LevelingCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +100,10 @@ public class LevelingCore extends JavaPlugin {
LevelingCore.configPath
);

public static final Map<String, Integer> mobOverrideMapping = MobOverrideMapping.loadOrCreate(
LevelingCore.configPath
);

public static final MobLevelRegistry mobLevelRegistry = new MobLevelRegistry();

public static final MobLevelPersistence mobLevelPersistence = new MobLevelPersistence();
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/azuredoom/levelingcore/config/GUIConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ public class GUIConfig {
(exConfig, extraInfo) -> exConfig.levelMode
)
.add()
.append(
new KeyedCodec<Integer>("LevelVariance", Codec.INTEGER),
(exConfig, anInteger, extraInfo) -> exConfig.levelVariance = anInteger,
(exConfig, extraInfo) -> exConfig.levelVariance
)
.add()
.append(
new KeyedCodec<Float>("MobHealthMultiplier", Codec.FLOAT),
(exConfig, aFloat, extraInfo) -> exConfig.mobHealthMultiplier = aFloat,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public record Bootstrap(
Map<String, Integer> mobZoneMapping,
Map<String, Integer> mobBiomeMapping,
Map<String, Integer> mobEnvironmentMapping,
Map<String, Integer> mobOverrideMapping,
AutoCloseable closeable
) {}

Expand Down Expand Up @@ -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,
Expand All @@ -87,6 +89,7 @@ public static Bootstrap bootstrap(Path dataDir) {
mobZoneMapping,
mobBiomeMapping,
mobEnvironmentMapping,
mobOverrideMapping,
repo::close
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Integer> 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<String, Integer> readXpCsv(Path csvPath) throws Exception {
Map<String, Integer> 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;
}
}
77 changes: 51 additions & 26 deletions src/main/java/com/azuredoom/levelingcore/utils/MobLevelingUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,29 @@ public static int computeDynamicLevel(
Store<EntityStore> 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);
});
}

Expand Down Expand Up @@ -97,7 +96,7 @@ public static int computeSpawnLevel(NPCEntity npc) {
return spawnMin + rng.nextInt((spawnMax - spawnMin) + 1);
}

public static int computeInstanceLevel(Store<EntityStore> store) {
public static int computeInstanceLevel(Store<EntityStore> store, NPCEntity npc) {
var world = store.getExternalData().getWorld();
var instanceName = world.getName();
var instanceMapping = LevelingCore.mobInstanceMapping;
Expand All @@ -107,21 +106,23 @@ public static int computeInstanceLevel(Store<EntityStore> 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<EntityStore> store) {
public static int computeZoneLevel(Store<EntityStore> store, NPCEntity npc) {
var world = store.getExternalData().getWorld();
var worldMapTracker = world.getPlayers().getFirst().getWorldMapTracker();
var currentZone = worldMapTracker.getCurrentZone();
if (currentZone == null)
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<EntityStore> store) {
public static int computeBiomeLevel(Store<EntityStore> store, NPCEntity npc) {
var world = store.getExternalData().getWorld();
var worldMapTracker = world.getPlayers().getFirst().getWorldMapTracker();
var currentBiome = worldMapTracker.getCurrentBiomeName();
Expand All @@ -130,11 +131,11 @@ public static int computeBiomeLevel(Store<EntityStore> 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<EntityStore> store) {
public static int computeEnvironmentLevel(TransformComponent transform, Store<EntityStore> store, NPCEntity npc) {
var world = store.getExternalData().getWorld();
var mobPos = transform.getPosition();
var chunk = world.getChunk(ChunkUtil.indexChunkFromBlock((int) mobPos.x, (int) mobPos.z));
Expand All @@ -160,11 +161,15 @@ public static int computeEnvironmentLevel(TransformComponent transform, Store<En
}

var environmentMapping = LevelingCore.mobEnvironmentMapping;

return environmentMapping.getOrDefault(envName.toLowerCase(), 1);
var baseLevel = environmentMapping.getOrDefault(envName.toLowerCase(), 1);
return randomizeLevel(baseLevel, npc);
}

public static int computeNearbyPlayersMeanLevel(TransformComponent transform, Store<EntityStore> store) {
public static int computeNearbyPlayersMeanLevel(
TransformComponent transform,
Store<EntityStore> store,
NPCEntity npc
) {
var world = store.getExternalData().getWorld();
var mobPos = transform.getPosition();
var players = world.getPlayers();
Expand All @@ -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);
}
}
2 changes: 2 additions & 0 deletions src/main/resources/defaultmoboverridemapping.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
npc_id,lvl
bunny,5
Loading