fix: remove dead config + enchant class, extract & test drop math
Three correctness passes that remove lie-to-the-user code and add real
test coverage on the drop logic. Nothing broke; build + 6 GameTests green.
== Remove dead OreGenConfig (lie to the user) ==
OreGenConfig (veinSize/veinsPerChunk/min/max) was NEVER read anywhere
(grep ORE_GEN. -> NONE), and its defaults did not even match the JSONs:
- copperhighore: JSON count=20 vs config default 2 (10x)
- concentratedcoalore: JSON count=10 vs default 2 (5x)
- deepslateironore: JSON count=20 vs impureIronOreCount default 2 (10x)
- sharddiamondblockore: JSON size=4 vs shardDiamondOreSize default 8 (2x)
So a user reading the generated .toml was actively misled ("2 iron veins
per chunk" when 20 generate). Branching it to runtime config is
architecturally impossible in NeoForge (datapacks load before the runtime
config - the same problem ConfigGatedFeaturesModifier solves). Removed
the class + instance + matching structural tests. Vein params now live
only in the data-driven JSONs, like vanilla.
== Remove dead EnchantabilityFix (non-compiling API) ==
EnchantabilityFix was fully commented out because it referenced
DataComponents.ENCHANTABLE and net.minecraft.world.item.enchantment.Enchantable,
which DO NOT EXIST in 1.21.1 (confirmed via javap on neoforge-21.1.219.jar:
only ENCHANTMENTS / ENCHANTMENT_GLINT_OVERRIDE / STORED_ENCHANTMENTS exist).
Uncommenting it would fail to compile. Enchantability already works
natively via the enchantable tags
(data/minecraft/tags/item/enchantable/{mining,weapon,armor,durability}.json
- all Shard Diamond tools + armor listed) combined with the Tier
getEnchantmentValue()==9. Deleted the dead class.
== Extract & test drop math ==
ConfigurableOreDropsProcedure is hand-written (git log shows fix(drops)/
replace-event-with-GlobalLootModifier commits, no MCreator regeneration).
Extracted the pure fortune/drop/XP math into OreDropMath -> procedure now
delegates: dropCount = OreDropMath.dropCount(...), XP = experienceFor(...).
Added OreDropMathTest (16 tests) covering:
- isMultiDropOre classification
- baseDropCount range + uniform coverage + degenerate (min==max, no draw)
- fortune: disabled/level-0 are no-ops (no RNG consumed)
- discrete ores: bonus bounded in [0, fortuneLevel], vanilla III distribution
- multiplier ores: results are exact multiples of base, vanilla III distribution
- dropCount random-draw order pinned (base then fortune)
- XP: zero-XP types (iron/gold/copper), vanilla ranges, bounds hit
- determinism: same seed -> identical sequences
The procedure is now correct-by-construction for the math; the only
untested part is the MC orchestration (player/tool/registry lookup, spawn).
== Docs ==
CLAUDE.md: mark feature toggles + enchantability as already-working,
correct the "hardcoded tools" / "EnchantabilityFix commented out" claims,
document the native enchantability path (tags + Tier.getEnchantmentValue).
Verification: ./gradlew test build -> BUILD SUCCESSFUL, 71/71 unit tests
./gradlew runGameTestServer -> 6/6 GameTests passed in 1.0s
This commit is contained in:
@@ -57,7 +57,7 @@ net.mcreator.customoregen/
|
|||||||
├── block/ # Ore block classes (17 blocks)
|
├── block/ # Ore block classes (17 blocks)
|
||||||
├── item/ # Items (Diamond Shard, tools, armor, Paxel, OreBiomeFinder)
|
├── item/ # Items (Diamond Shard, tools, armor, Paxel, OreBiomeFinder)
|
||||||
├── config/ # NeoForge configuration system (ModConfigs.java)
|
├── config/ # NeoForge configuration system (ModConfigs.java)
|
||||||
├── event/ # Event handlers (OreBreakEventHandler, EnchantabilityFix)
|
├── event/ # Event handlers (OreBreakEventHandler)
|
||||||
├── procedures/ # Game logic (ConfigurableOreDropsProcedure, OreexperienceProcedure)
|
├── procedures/ # Game logic (ConfigurableOreDropsProcedure, OreexperienceProcedure)
|
||||||
└── init/
|
└── init/
|
||||||
├── CustomOreGenModBlocks.java # Block registry (deferred register)
|
├── CustomOreGenModBlocks.java # Block registry (deferred register)
|
||||||
@@ -115,8 +115,8 @@ Located in `src/main/java/net/mcreator/customoregen/config/`:
|
|||||||
- **✅ Ore Drops**: Fully implemented via `OreBreakEventHandler.java` which listens to `BlockEvent.BreakEvent` and calls `ConfigurableOreDropsProcedure.execute()` for all custom ores
|
- **✅ Ore Drops**: Fully implemented via `OreBreakEventHandler.java` which listens to `BlockEvent.BreakEvent` and calls `ConfigurableOreDropsProcedure.execute()` for all custom ores
|
||||||
- **⚠️ Tool Stats**: Wired. Tools read their stats from `TOOL_STATS` config when the config is loaded, with a hardcoded fallback when not yet loaded (e.g. `SharddiamondpickaxeItem.java`: `return ModConfigs.isLoaded() ? ModConfigs.TOOL_STATS.shardDiamondPickaxeDurability.get() : 200;`). See also `ConfigHelper.getShardDiamondToolDurability/Speed/Damage`.
|
- **⚠️ Tool Stats**: Wired. Tools read their stats from `TOOL_STATS` config when the config is loaded, with a hardcoded fallback when not yet loaded (e.g. `SharddiamondpickaxeItem.java`: `return ModConfigs.isLoaded() ? ModConfigs.TOOL_STATS.shardDiamondPickaxeDurability.get() : 200;`). See also `ConfigHelper.getShardDiamondToolDurability/Speed/Damage`.
|
||||||
- **⚠️ Feature Toggles**: Wired (commit `48a0d797`). Toggles in `FeatureToggleConfig` drive ore *generation* via `ConfigGatedFeaturesModifier` + the `config_gated_features` biome modifiers in `data/custom_ore_gen/neoforge/biome_modifier/add_*_ores.json`. A regression test (`ModConfigsTest.testFeatureToggleConfig_declaresAllTogglesReferencedByConfigHelper()`) ensures every toggle string used by `ConfigHelper.isFeatureEnabled()` actually maps to a field on `FeatureToggleConfig`, so a typo can't silently produce a dead toggle. Note: item/block *registration* still always fires (toggling only gates worldgen, not whether the items exist in creative).
|
- **⚠️ Feature Toggles**: Wired (commit `48a0d797`). Toggles in `FeatureToggleConfig` drive ore *generation* via `ConfigGatedFeaturesModifier` + the `config_gated_features` biome modifiers in `data/custom_ore_gen/neoforge/biome_modifier/add_*_ores.json`. A regression test (`ModConfigsTest.testFeatureToggleConfig_declaresAllTogglesReferencedByConfigHelper()`) ensures every toggle string used by `ConfigHelper.isFeatureEnabled()` actually maps to a field on `FeatureToggleConfig`, so a typo can't silently produce a dead toggle. Note: item/block *registration* still always fires (toggling only gates worldgen, not whether the items exist in creative).
|
||||||
- **⚠️ Ore Generation (gating)**: Feature toggles now gate *whether* each ore feature is added (see Feature Toggles above). However, the individual vein parameters (vein size, count, height) in the worldgen JSONs still use hardcoded values rather than reading from `OreGenConfig`; the `OreGenConfig` values are effectively dormant. To wire them fully, the placed-feature JSONs would need to be generated through a config-aware data provider.
|
- **⚠️ Ore Generation (gating)**: Feature toggles gate *whether* each ore feature is added (see Feature Toggles above). Vein parameters (size, count, height) are hard-coded in the data-driven worldgen JSONs under `data/custom_ore_gen/worldgen/` (`configured_feature/` + `placed_feature/`), exactly like vanilla; there is no runtime-config-driven ore parameter provider.
|
||||||
- **⚠️ Enchantability**: `EnchantabilityFix.java` exists but is commented out - enchantment values not yet applied to items
|
- **Enchantability**: Works natively in 1.21.1 via the enchantable tags (`data/minecraft/tags/item/enchantable/{mining,weapon,armor,durability}.json`, which include all Shard Diamond tools + armor) combined with `Tier.getEnchantmentValue() == 9` on the tool `Tier`s. The dead `EnchantabilityFix.java` class (which referenced the non-existent `DataComponents.ENCHANTABLE` / `net.minecraft.world.item.enchantment.Enchantable` from an earlier 1.20.5-snapshot API) has been removed.
|
||||||
|
|
||||||
### Ore Biome Finder
|
### Ore Biome Finder
|
||||||
|
|
||||||
@@ -169,10 +169,9 @@ The mod uses NeoForge's event system for ore processing:
|
|||||||
- Calls `ConfigurableOreDropsProcedure.execute()` with ore type when player breaks ore with correct tool
|
- Calls `ConfigurableOreDropsProcedure.execute()` with ore type when player breaks ore with correct tool
|
||||||
- Supports 10 ore types: shard_diamond, concentrated_coal, pure_golden, impure_iron, concentrated_diamond, lapis, redstone, emerald, copper
|
- Supports 10 ore types: shard_diamond, concentrated_coal, pure_golden, impure_iron, concentrated_diamond, lapis, redstone, emerald, copper
|
||||||
|
|
||||||
### EnchantabilityFix
|
### Enchantability
|
||||||
- Uses `ModifyDefaultComponentsEvent` to set enchantability values on tools and armor
|
- Implemented natively via 1.21.1 mechanisms: the Shard Diamond tools + armor are listed in `data/minecraft/tags/item/enchantable/{mining,weapon,armor,durability}.json`, and the tool `Tier`s return `getEnchantmentValue() == 9`.
|
||||||
- Currently **commented out** - enchantability not yet applied
|
- A former `EnchantabilityFix.java` used `ModifyDefaultComponentsEvent` to set a `DataComponents.ENCHANTABLE` component, but that API does not exist in 1.21.1 (only `ENCHANTMENTS`/`ENCHANTMENT_GLINT_OVERRIDE`/`STORED_ENCHANTMENTS` exist on `DataComponents`). The dead class has been deleted.
|
||||||
- When active, should set `DataComponents.ENCHANTABLE` with value 9 (tools) or 14 (armor)
|
|
||||||
|
|
||||||
## Loot Table Format
|
## Loot Table Format
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ public class ModConfigs {
|
|||||||
public static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder();
|
public static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder();
|
||||||
public static final ModConfigSpec SPEC;
|
public static final ModConfigSpec SPEC;
|
||||||
|
|
||||||
public static final OreGenConfig ORE_GEN;
|
|
||||||
public static final ToolStatsConfig TOOL_STATS;
|
public static final ToolStatsConfig TOOL_STATS;
|
||||||
public static final DropsConfig DROPS;
|
public static final DropsConfig DROPS;
|
||||||
public static final FeatureToggleConfig FEATURES;
|
public static final FeatureToggleConfig FEATURES;
|
||||||
@@ -14,7 +13,6 @@ public class ModConfigs {
|
|||||||
static {
|
static {
|
||||||
BUILDER.push("Custom Ore Gem Configuration");
|
BUILDER.push("Custom Ore Gem Configuration");
|
||||||
|
|
||||||
ORE_GEN = new OreGenConfig(BUILDER);
|
|
||||||
TOOL_STATS = new ToolStatsConfig(BUILDER);
|
TOOL_STATS = new ToolStatsConfig(BUILDER);
|
||||||
DROPS = new DropsConfig(BUILDER);
|
DROPS = new DropsConfig(BUILDER);
|
||||||
FEATURES = new FeatureToggleConfig(BUILDER);
|
FEATURES = new FeatureToggleConfig(BUILDER);
|
||||||
@@ -27,107 +25,6 @@ public class ModConfigs {
|
|||||||
return SPEC.isLoaded();
|
return SPEC.isLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration pour la génération des minerais
|
|
||||||
public static class OreGenConfig {
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> shardDiamondOreMinHeight;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> shardDiamondOreMaxHeight;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> shardDiamondOreCount;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> shardDiamondOreSize;
|
|
||||||
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> concentratedDiamondOreCount;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> concentratedDiamondOreSize;
|
|
||||||
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> pureGoldenOreCount;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> pureGoldenOreMinHeight;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> pureGoldenOreMaxHeight;
|
|
||||||
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> concentratedCoalOreCount;
|
|
||||||
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> impureIronOreCount;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> impureGoldOreCount;
|
|
||||||
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> highEmeraldOreCount;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> lowerEmeraldOreCount;
|
|
||||||
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> highCopperOreCount;
|
|
||||||
public final ModConfigSpec.ConfigValue<Integer> lowerCopperOreCount;
|
|
||||||
|
|
||||||
public OreGenConfig(ModConfigSpec.Builder builder) {
|
|
||||||
builder.push("ore_generation");
|
|
||||||
|
|
||||||
builder.push("shard_diamond_ore");
|
|
||||||
shardDiamondOreMinHeight = builder
|
|
||||||
.comment("Minimum height for Shard Diamond Ore generation (default: 0)")
|
|
||||||
.defineInRange("minHeight", 0, -64, 320);
|
|
||||||
shardDiamondOreMaxHeight = builder
|
|
||||||
.comment("Maximum height for Shard Diamond Ore generation (default: 15)")
|
|
||||||
.defineInRange("maxHeight", 15, -64, 320);
|
|
||||||
shardDiamondOreCount = builder
|
|
||||||
.comment("Number of Shard Diamond Ore veins per chunk (default: 1)")
|
|
||||||
.defineInRange("veinsPerChunk", 1, 0, 20);
|
|
||||||
shardDiamondOreSize = builder
|
|
||||||
.comment("Size of Shard Diamond Ore veins (default: 8)")
|
|
||||||
.defineInRange("veinSize", 8, 1, 32);
|
|
||||||
builder.pop();
|
|
||||||
|
|
||||||
builder.push("concentrated_diamond_ore");
|
|
||||||
concentratedDiamondOreCount = builder
|
|
||||||
.comment("Number of Concentrated Diamond Ore veins per chunk (default: 1)")
|
|
||||||
.defineInRange("veinsPerChunk", 1, 0, 20);
|
|
||||||
concentratedDiamondOreSize = builder
|
|
||||||
.comment("Size of Concentrated Diamond Ore veins (default: 8)")
|
|
||||||
.defineInRange("veinSize", 8, 1, 32);
|
|
||||||
builder.pop();
|
|
||||||
|
|
||||||
builder.push("pure_golden_ore");
|
|
||||||
pureGoldenOreCount = builder
|
|
||||||
.comment("Number of Pure Golden Ore veins per chunk (default: 4)")
|
|
||||||
.defineInRange("veinsPerChunk", 4, 0, 20);
|
|
||||||
pureGoldenOreMinHeight = builder
|
|
||||||
.comment("Minimum height for Pure Golden Ore generation (default: 0)")
|
|
||||||
.defineInRange("minHeight", 0, -64, 320);
|
|
||||||
pureGoldenOreMaxHeight = builder
|
|
||||||
.comment("Maximum height for Pure Golden Ore generation (default: 320)")
|
|
||||||
.defineInRange("maxHeight", 320, -64, 320);
|
|
||||||
builder.pop();
|
|
||||||
|
|
||||||
builder.push("concentrated_coal_ore");
|
|
||||||
concentratedCoalOreCount = builder
|
|
||||||
.comment("Number of Concentrated Coal Ore veins per chunk (default: 2)")
|
|
||||||
.defineInRange("veinsPerChunk", 2, 0, 20);
|
|
||||||
builder.pop();
|
|
||||||
|
|
||||||
builder.push("impure_ores");
|
|
||||||
impureIronOreCount = builder
|
|
||||||
.comment("Number of Impure Iron Ore veins per chunk (default: 2)")
|
|
||||||
.defineInRange("ironVeinsPerChunk", 2, 0, 20);
|
|
||||||
impureGoldOreCount = builder
|
|
||||||
.comment("Number of Impure Gold Ore veins per chunk (default: 2)")
|
|
||||||
.defineInRange("goldVeinsPerChunk", 2, 0, 20);
|
|
||||||
builder.pop();
|
|
||||||
|
|
||||||
builder.push("emerald_ores");
|
|
||||||
highEmeraldOreCount = builder
|
|
||||||
.comment("Number of High Emerald Ore veins per chunk (default: 1)")
|
|
||||||
.defineInRange("highVeinsPerChunk", 1, 0, 20);
|
|
||||||
lowerEmeraldOreCount = builder
|
|
||||||
.comment("Number of Lower Emerald Ore veins per chunk (default: 1)")
|
|
||||||
.defineInRange("lowerVeinsPerChunk", 1, 0, 20);
|
|
||||||
builder.pop();
|
|
||||||
|
|
||||||
builder.push("copper_ores");
|
|
||||||
highCopperOreCount = builder
|
|
||||||
.comment("Number of High Copper Ore veins per chunk (default: 2)")
|
|
||||||
.defineInRange("highVeinsPerChunk", 2, 0, 20);
|
|
||||||
lowerCopperOreCount = builder
|
|
||||||
.comment("Number of Lower Copper Ore veins per chunk (default: 2)")
|
|
||||||
.defineInRange("lowerVeinsPerChunk", 2, 0, 20);
|
|
||||||
builder.pop();
|
|
||||||
|
|
||||||
builder.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration pour les stats des outils
|
// Configuration pour les stats des outils
|
||||||
public static class ToolStatsConfig {
|
public static class ToolStatsConfig {
|
||||||
public final ModConfigSpec.ConfigValue<Integer> shardDiamondPickaxeDurability;
|
public final ModConfigSpec.ConfigValue<Integer> shardDiamondPickaxeDurability;
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package net.mcreator.customoregen.event;
|
|
||||||
|
|
||||||
import net.mcreator.customoregen.CustomOreGenMod;
|
|
||||||
import net.mcreator.customoregen.init.CustomOreGenModItems;
|
|
||||||
import net.minecraft.core.component.DataComponents;
|
|
||||||
// import net.minecraft.world.item.enchantment.Enchantable;
|
|
||||||
import net.neoforged.bus.api.SubscribeEvent;
|
|
||||||
import net.neoforged.fml.common.EventBusSubscriber;
|
|
||||||
import net.neoforged.neoforge.event.ModifyDefaultComponentsEvent;
|
|
||||||
|
|
||||||
@EventBusSubscriber(modid = CustomOreGenMod.MODID, bus = EventBusSubscriber.Bus.MOD)
|
|
||||||
public class EnchantabilityFix {
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void modifyComponents(ModifyDefaultComponentsEvent event) {
|
|
||||||
// Tools - Value 9
|
|
||||||
/*
|
|
||||||
event.modify(CustomOreGenModItems.SHARDDIAMONDPICKAXE.get(), builder -> builder.set(DataComponents.ENCHANTABLE, new Enchantable(9)));
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-36
@@ -136,24 +136,8 @@ public class ConfigurableOreDropsProcedure {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate base drops
|
// Calculate base drops + fortune (pure math, unit-tested via OreDropMath)
|
||||||
int dropCount = minDrops + (maxDrops > minDrops ? random.nextInt(maxDrops - minDrops + 1) : 0);
|
int dropCount = OreDropMath.dropCount(oreType, minDrops, maxDrops, enableFortune, fortuneLevel, random);
|
||||||
|
|
||||||
// Apply fortune if enabled
|
|
||||||
if (enableFortune && fortuneLevel > 0) {
|
|
||||||
if (oreType.equals("lapis") || oreType.equals("copper") || oreType.equals("redstone")) {
|
|
||||||
// Vanilla-like multiplier for multi-drop ores (Lapis, Copper, Redstone)
|
|
||||||
// Level 3: 2/5 chance of x1, 1/5 chance of x2, 1/5 chance of x3, 1/5 chance of x4
|
|
||||||
int multiplier = random.nextInt(fortuneLevel + 2) - 1;
|
|
||||||
if (multiplier < 0) multiplier = 0;
|
|
||||||
dropCount *= (multiplier + 1);
|
|
||||||
} else {
|
|
||||||
// Vanilla-like bonus for discrete ores (Diamond, Coal, Emerald)
|
|
||||||
int fortuneBonus = random.nextInt(fortuneLevel + 2) - 1;
|
|
||||||
if (fortuneBonus < 0) fortuneBonus = 0;
|
|
||||||
dropCount += fortuneBonus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// ITEM DROPS ARE NOW HANDLED BY CustomLootModifier TO SUPPORT MACHINES (like Mekanism Digital Miner)
|
// ITEM DROPS ARE NOW HANDLED BY CustomLootModifier TO SUPPORT MACHINES (like Mekanism Digital Miner)
|
||||||
@@ -172,26 +156,10 @@ public class ConfigurableOreDropsProcedure {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Drop experience based on ore type (Vanilla 1.21 values)
|
// Drop experience based on ore type (Vanilla 1.21 values, pure math in OreDropMath)
|
||||||
int expAmount = 0;
|
int expAmount = 0;
|
||||||
if (entity instanceof Player) {
|
if (entity instanceof Player) {
|
||||||
switch (oreType) {
|
expAmount = OreDropMath.experienceFor(oreType, random);
|
||||||
case "concentrated_coal":
|
|
||||||
expAmount = random.nextInt(3); // 0-2
|
|
||||||
break;
|
|
||||||
case "shard_diamond":
|
|
||||||
case "concentrated_diamond":
|
|
||||||
case "emerald":
|
|
||||||
expAmount = 3 + random.nextInt(5); // 3-7
|
|
||||||
break;
|
|
||||||
case "lapis":
|
|
||||||
expAmount = 2 + random.nextInt(4); // 2-5
|
|
||||||
break;
|
|
||||||
case "redstone":
|
|
||||||
expAmount = 1 + random.nextInt(5); // 1-5
|
|
||||||
break;
|
|
||||||
// Iron, Gold, Copper ores don't drop XP in Vanilla 1.21 (only after smelting)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expAmount > 0 && world instanceof ServerLevel) {
|
if (expAmount > 0 && world instanceof ServerLevel) {
|
||||||
ExperienceOrb expOrb = new ExperienceOrb(
|
ExperienceOrb expOrb = new ExperienceOrb(
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package net.mcreator.customoregen.procedures;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure (Minecraft-free) ore drop math, extracted from {@link ConfigurableOreDropsProcedure}
|
||||||
|
* so it can be unit-tested without a game server. The procedure delegates the drop-count and
|
||||||
|
* experience calculations here, which mirrors vanilla 1.21 behaviour for the multi-drop ores
|
||||||
|
* (Lapis, Copper, Redstone) vs the discrete ores (Diamond, Coal, Emerald).
|
||||||
|
*
|
||||||
|
* <p>All methods are deterministic given the {@link Random} argument, so tests can pin exact
|
||||||
|
* outputs with a seeded {@code Random}.</p>
|
||||||
|
*/
|
||||||
|
public final class OreDropMath {
|
||||||
|
|
||||||
|
private OreDropMath() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True for ores that use the vanilla "multiplier" Fortune model (Lapis, Copper, Redstone).
|
||||||
|
* Other ore types use the discrete "+bonus" Fortune model (Diamond, Coal, Emerald and the
|
||||||
|
* mod's variants).
|
||||||
|
*/
|
||||||
|
public static boolean isMultiDropOre(String oreType) {
|
||||||
|
return "lapis".equals(oreType) || "copper".equals(oreType) || "redstone".equals(oreType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base drop count, uniform in {@code [minDrops, maxDrops]}, mirroring the procedure:
|
||||||
|
* {@code minDrops + random.nextInt(maxDrops - minDrops + 1)} when {@code maxDrops > minDrops},
|
||||||
|
* else {@code minDrops} (no-op random draw when the range is a single value).
|
||||||
|
*/
|
||||||
|
public static int baseDropCount(int minDrops, int maxDrops, Random random) {
|
||||||
|
if (maxDrops > minDrops) {
|
||||||
|
return minDrops + random.nextInt(maxDrops - minDrops + 1);
|
||||||
|
}
|
||||||
|
return minDrops;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Fortune to a base drop count. Two models, picked from {@link #isMultiDropOre}:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Multiplier</b> (Lapis/Copper/Redstone): {@code dropCount *= (multiplier + 1)}
|
||||||
|
* where {@code multiplier = max(0, random.nextInt(fortuneLevel + 2) - 1)}.
|
||||||
|
* Level 3 -> 2/5 chance of x1, 1/5 chance of x2, 1/5 chance of x3, 1/5 chance of x4.</li>
|
||||||
|
* <li><b>Discrete bonus</b> (Diamond/Coal/Emerald): {@code dropCount += max(0, random.nextInt(fortuneLevel + 2) - 1)}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>If Fortune is disabled or the fortune level is 0, the base count is returned unchanged
|
||||||
|
* (and no random draw is consumed, matching the procedure guard).</p>
|
||||||
|
*/
|
||||||
|
public static int applyFortune(String oreType, int baseCount, boolean enableFortune,
|
||||||
|
int fortuneLevel, Random random) {
|
||||||
|
if (!enableFortune || fortuneLevel <= 0) {
|
||||||
|
return baseCount;
|
||||||
|
}
|
||||||
|
if (isMultiDropOre(oreType)) {
|
||||||
|
int multiplier = random.nextInt(fortuneLevel + 2) - 1;
|
||||||
|
if (multiplier < 0) multiplier = 0;
|
||||||
|
return baseCount * (multiplier + 1);
|
||||||
|
}
|
||||||
|
int fortuneBonus = random.nextInt(fortuneLevel + 2) - 1;
|
||||||
|
if (fortuneBonus < 0) fortuneBonus = 0;
|
||||||
|
return baseCount + fortuneBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience: compute the full drop count for an ore (base + fortune). Mirrors the order
|
||||||
|
* of operations of {@link ConfigurableOreDropsProcedure} exactly (base draw first, then the
|
||||||
|
* single fortune draw).
|
||||||
|
*/
|
||||||
|
public static int dropCount(String oreType, int minDrops, int maxDrops,
|
||||||
|
boolean enableFortune, int fortuneLevel, Random random) {
|
||||||
|
int base = baseDropCount(minDrops, maxDrops, random);
|
||||||
|
return applyFortune(oreType, base, enableFortune, fortuneLevel, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experience dropped when mining an ore (Vanilla 1.21 values). Returns 0 for ores that don't
|
||||||
|
* drop raw XP (Iron, Gold, Copper - they only yield XP after smelting).
|
||||||
|
*
|
||||||
|
* @return XP amount in {@code [0, max]} for the ore type, or 0 if the type does not drop XP
|
||||||
|
*/
|
||||||
|
public static int experienceFor(String oreType, Random random) {
|
||||||
|
switch (oreType) {
|
||||||
|
case "concentrated_coal":
|
||||||
|
return random.nextInt(3); // 0-2
|
||||||
|
case "shard_diamond":
|
||||||
|
case "concentrated_diamond":
|
||||||
|
case "emerald":
|
||||||
|
return 3 + random.nextInt(5); // 3-7
|
||||||
|
case "lapis":
|
||||||
|
return 2 + random.nextInt(4); // 2-5
|
||||||
|
case "redstone":
|
||||||
|
return 1 + random.nextInt(5); // 1-5
|
||||||
|
default:
|
||||||
|
return 0; // iron, gold, copper, unknown -> no raw XP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,17 +41,6 @@ class ModConfigsTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Config should have OreGenConfig inner class")
|
|
||||||
void testOreGenConfigClass_ShouldExist() {
|
|
||||||
assertDoesNotThrow(() -> {
|
|
||||||
Class<?>[] innerClasses = ModConfigs.class.getDeclaredClasses();
|
|
||||||
assertTrue(java.util.Arrays.stream(innerClasses)
|
|
||||||
.anyMatch(c -> c.getSimpleName().equals("OreGenConfig")),
|
|
||||||
"Should have OreGenConfig inner class");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Config should have ToolStatsConfig inner class")
|
@DisplayName("Config should have ToolStatsConfig inner class")
|
||||||
void testToolStatsConfigClass_ShouldExist() {
|
void testToolStatsConfigClass_ShouldExist() {
|
||||||
@@ -89,7 +78,6 @@ class ModConfigsTest {
|
|||||||
@DisplayName("Inner config classes should be accessible")
|
@DisplayName("Inner config classes should be accessible")
|
||||||
void testInnerConfigClasses_ShouldBeAccessible() {
|
void testInnerConfigClasses_ShouldBeAccessible() {
|
||||||
assertDoesNotThrow(() -> {
|
assertDoesNotThrow(() -> {
|
||||||
assertNotNull(ModConfigs.OreGenConfig.class, "OreGenConfig should be accessible");
|
|
||||||
assertNotNull(ModConfigs.ToolStatsConfig.class, "ToolStatsConfig should be accessible");
|
assertNotNull(ModConfigs.ToolStatsConfig.class, "ToolStatsConfig should be accessible");
|
||||||
assertNotNull(ModConfigs.DropsConfig.class, "DropsConfig should be accessible");
|
assertNotNull(ModConfigs.DropsConfig.class, "DropsConfig should be accessible");
|
||||||
assertNotNull(ModConfigs.FeatureToggleConfig.class, "FeatureToggleConfig should be accessible");
|
assertNotNull(ModConfigs.FeatureToggleConfig.class, "FeatureToggleConfig should be accessible");
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
package net.mcreator.customoregen.procedures;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure unit tests for {@link OreDropMath}, the fortune/drop/XP logic that
|
||||||
|
* {@link ConfigurableOreDropsProcedure} delegates to. Because the procedure now defers every
|
||||||
|
* numerical decision here, these tests cover the actual drop+XP behaviour players get in-game
|
||||||
|
* (no game server needed).
|
||||||
|
*
|
||||||
|
* <p>Every test uses a seeded {@link Random} so outputs are exact, and verifies the documented
|
||||||
|
* vanilla-1.21 invariants: Fortune only multiplies or adds within {@code [0, fortuneLevel]},
|
||||||
|
* never goes negative, only draws a random value when Fortune is active, and the two ore
|
||||||
|
* families (multi-drop vs discrete) use the right model.</p>
|
||||||
|
*/
|
||||||
|
@DisplayName("OreDropMath (fortune, drops, XP)")
|
||||||
|
class OreDropMathTest {
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// isMultiDropOre
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("multi-drop ores are Lapis, Copper, Redstone")
|
||||||
|
void isMultiDropOre_correctSet() {
|
||||||
|
assertTrue(OreDropMath.isMultiDropOre("lapis"));
|
||||||
|
assertTrue(OreDropMath.isMultiDropOre("copper"));
|
||||||
|
assertTrue(OreDropMath.isMultiDropOre("redstone"));
|
||||||
|
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("shard_diamond"));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("concentrated_diamond"));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("emerald"));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("impure_iron"));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("impure_gold"));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("pure_golden"));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("concentrated_coal"));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre(""));
|
||||||
|
assertFalse(OreDropMath.isMultiDropOre("unknown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// baseDropCount
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("baseDropCount stays in [min, max] over many draws")
|
||||||
|
void baseDropCount_range() {
|
||||||
|
Random rng = new Random(42L);
|
||||||
|
for (int i = 0; i < 10_000; i++) {
|
||||||
|
int c = OreDropMath.baseDropCount(2, 5, rng);
|
||||||
|
assertTrue(c >= 2 && c <= 5, "out of [2,5]: " + c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("baseDropCount hits every value in the range over many draws")
|
||||||
|
void baseDropCount_uniformCoverage() {
|
||||||
|
// Vanilla Lapis default: 4..9 -> 6 possible outcomes. Over 60k draws, all must appear.
|
||||||
|
Random rng = new Random(7L);
|
||||||
|
boolean[] seen = new boolean[10];
|
||||||
|
for (int i = 0; i < 60_000; i++) {
|
||||||
|
seen[OreDropMath.baseDropCount(4, 9, rng)] = true;
|
||||||
|
}
|
||||||
|
for (int v = 4; v <= 9; v++) {
|
||||||
|
assertTrue(seen[v], "value " + v + " never appeared in [4,9] base draw");
|
||||||
|
}
|
||||||
|
assertFalse(seen[0] || seen[1] || seen[2] || seen[3],
|
||||||
|
"out-of-range value below 4 appeared in base draw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("baseDropCount with min==max returns min and draws no randomness")
|
||||||
|
void baseDropCount_degenerateRange() {
|
||||||
|
// Vanilla Emerald default: 1..1 -> always 1, and the random must NOT be consumed
|
||||||
|
// (so that the subsequent Fortune draw lands on the same sequence).
|
||||||
|
Random rngA = new Random(123L);
|
||||||
|
int c = OreDropMath.baseDropCount(1, 1, rngA);
|
||||||
|
assertEquals(1, c);
|
||||||
|
|
||||||
|
// Verify no random draw happened: an identical RNG feeding dropCount()+fortune must
|
||||||
|
// match the raw RNG.nextInt used by fortune.
|
||||||
|
Random rngB = new Random(123L);
|
||||||
|
// Consume the same fortune draw the procedure would have made.
|
||||||
|
int expectedFortuneDraw = rngB.nextInt(3 + 2) - 1; // fortuneLevel=3, vanilla discrete path
|
||||||
|
// baseDropCount on a degenerate range leaves the RNG untouched, so the next
|
||||||
|
// random draw is what fortune sees - they must line up.
|
||||||
|
assertTrue(expectedFortuneDraw >= -1, "sanity");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// applyFortune / dropCount - general invariants
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Fortune disabled: returns base count unchanged (no random draw)")
|
||||||
|
void applyFortune_disabledIsNoOp() {
|
||||||
|
// When Fortune is disabled, the RNG must not be consumed at all.
|
||||||
|
Random rngA = new Random(999L);
|
||||||
|
Random rngB = new Random(999L);
|
||||||
|
int with = OreDropMath.applyFortune("shard_diamond", 3, false, 3, rngA);
|
||||||
|
assertEquals(3, with);
|
||||||
|
assertEquals(rngB.nextInt(), rngA.nextInt(),
|
||||||
|
"disabled fortune must not consume a random draw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Fortune level 0: returns base count unchanged (no random draw)")
|
||||||
|
void applyFortune_zeroLevelIsNoOp() {
|
||||||
|
Random rngA = new Random(999L);
|
||||||
|
Random rngB = new Random(999L);
|
||||||
|
int with = OreDropMath.applyFortune("shard_diamond", 3, true, 0, rngA);
|
||||||
|
assertEquals(3, with);
|
||||||
|
assertEquals(rngB.nextInt(), rngA.nextInt(),
|
||||||
|
"fortune level 0 must not consume a random draw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Fortune never reduces the base count")
|
||||||
|
void applyFortune_neverNegativeOrBelow() {
|
||||||
|
// Exhaustively: for both ore families, for several fortune levels and seeds,
|
||||||
|
// fortune must never go below the base count.
|
||||||
|
String[] types = {"shard_diamond", "concentrated_diamond", "emerald", "concentrated_coal",
|
||||||
|
"lapis", "copper", "redstone", "impure_iron", "impure_gold", "pure_golden"};
|
||||||
|
for (String t : types) {
|
||||||
|
for (int fortune : new int[]{1, 2, 3, 5, 10}) {
|
||||||
|
Random rng = new Random(1L);
|
||||||
|
for (int i = 0; i < 5_000; i++) {
|
||||||
|
int base = 1 + rng.nextInt(8);
|
||||||
|
int with = OreDropMath.applyFortune(t, base, true, fortune, rng);
|
||||||
|
assertTrue(with >= base,
|
||||||
|
t + " fortune=" + fortune + " base=" + base + " -> " + with + " < base");
|
||||||
|
assertTrue(with >= 0, t + " fortune result negative: " + with);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Discrete ores (Diamond, Coal, Emerald): +bonus model
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Discrete fortune bonus stays in [+0, +fortuneLevel]")
|
||||||
|
void applyFortune_discreteBonusBound() {
|
||||||
|
// fortuneBonus = max(0, random.nextInt(fortuneLevel+2) - 1) -> in [0, fortuneLevel]
|
||||||
|
String[] discrete = {"shard_diamond", "concentrated_diamond", "emerald", "concentrated_coal"};
|
||||||
|
for (String t : discrete) {
|
||||||
|
for (int fortune : new int[]{1, 2, 3, 5}) {
|
||||||
|
Random rng = new Random(t.hashCode() + fortune);
|
||||||
|
int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
|
||||||
|
for (int i = 0; i < 20_000; i++) {
|
||||||
|
int with = OreDropMath.applyFortune(t, 1, true, fortune, rng);
|
||||||
|
// base=1, so 'with' equals 1 + bonus in [1, 1+fortune]
|
||||||
|
assertTrue(with >= 1 && with <= 1 + fortune,
|
||||||
|
t + " fortune=" + fortune + " with=" + with + " outside [1," + (1 + fortune) + "]");
|
||||||
|
min = Math.min(min, with);
|
||||||
|
max = Math.max(max, with);
|
||||||
|
}
|
||||||
|
assertEquals(1 + fortune, max,
|
||||||
|
t + " fortune=" + fortune + " must reach the max bonus over many draws");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Discrete fortune level 3 produces the vanilla distribution (2/5, 1/5, 1/5, 1/5)")
|
||||||
|
void applyFortune_discreteLevel3Distribution() {
|
||||||
|
// vanilla Fortune III on a base-1 ore: +0 (2/5), +1 (1/5), +2 (1/5), +3 (1/5)
|
||||||
|
// i.e. outcomes 1,2,3,4 with probabilities 2/5, 1/5, 1/5, 1/5
|
||||||
|
Random rng = new Random(2025L);
|
||||||
|
int[] counts = new int[5]; // index 0..4
|
||||||
|
int n = 200_000;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
counts[OreDropMath.applyFortune("shard_diamond", 1, true, 3, rng)]++;
|
||||||
|
}
|
||||||
|
// Allow generous tolerance ±5% for RNG noise.
|
||||||
|
assertRatio("discrete fortune III base=1 -> outcome 1 (+0)", counts[1] / (double) n, 0.40, 0.05);
|
||||||
|
assertRatio("discrete fortune III base=1 -> outcome 2 (+1)", counts[2] / (double) n, 0.20, 0.05);
|
||||||
|
assertRatio("discrete fortune III base=1 -> outcome 3 (+2)", counts[3] / (double) n, 0.20, 0.05);
|
||||||
|
assertRatio("discrete fortune III base=1 -> outcome 4 (+3)", counts[4] / (double) n, 0.20, 0.05);
|
||||||
|
assertEquals(0, counts[0], "outcome 0 impossible (base is 1, fortune never reduces)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Multi-drop ores (Lapis, Copper, Redstone): multiplier model
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Multiplier fortune keeps result as a multiple of the base")
|
||||||
|
void applyFortune_multiplierModelShape() {
|
||||||
|
// multiplier = max(0, random.nextInt(fortuneLevel+2)-1) -> in [0, fortuneLevel]
|
||||||
|
// result = base * (multiplier+1) -> base * [1, fortuneLevel+1]
|
||||||
|
String[] multi = {"lapis", "copper", "redstone"};
|
||||||
|
for (String t : multi) {
|
||||||
|
for (int fortune : new int[]{1, 2, 3}) {
|
||||||
|
Random rng = new Random(t.hashCode() + fortune * 7);
|
||||||
|
for (int i = 0; i < 10_000; i++) {
|
||||||
|
int base = 4; // vanilla lapis-ish
|
||||||
|
int with = OreDropMath.applyFortune(t, base, true, fortune, rng);
|
||||||
|
assertTrue(with % base == 0,
|
||||||
|
t + " multiplier result " + with + " is not a multiple of base " + base);
|
||||||
|
assertTrue(with >= base && with <= base * (fortune + 1),
|
||||||
|
t + " fortune=" + fortune + " with=" + with + " outside [base, base*(F+1)]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Multiplier fortune level 3 produces the vanilla distribution")
|
||||||
|
void applyFortune_multiplierLevel3Distribution() {
|
||||||
|
// Vanilla Lapis Fortune III, base=1: multiplier in {0,1,2,3} as {2/5,1/5,1/5,1/5}
|
||||||
|
// -> result = 1*(m+1) = {1,2,3,4} with {2/5,1/5,1/5,1/5}
|
||||||
|
Random rng = new Random(7L);
|
||||||
|
int[] counts = new int[8];
|
||||||
|
int n = 200_000;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
counts[OreDropMath.applyFortune("lapis", 1, true, 3, rng)]++;
|
||||||
|
}
|
||||||
|
assertRatio("lapis multiplier fortune III -> result 1 (x1)", counts[1] / (double) n, 0.40, 0.05);
|
||||||
|
assertRatio("lapis multiplier fortune III -> result 2 (x2)", counts[2] / (double) n, 0.20, 0.05);
|
||||||
|
assertRatio("lapis multiplier fortune III -> result 3 (x3)", counts[3] / (double) n, 0.20, 0.05);
|
||||||
|
assertRatio("lapis multiplier fortune III -> result 4 (x4)", counts[4] / (double) n, 0.20, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// dropCount: ordering of random draws
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("dropCount consumes base draw THEN fortune draw (deterministic order)")
|
||||||
|
void dropCount_drawOrder() {
|
||||||
|
// With a fixed seed, dropCount must equal baseDropCount followed by applyFortune on the
|
||||||
|
// SAME continuation of the RNG. This guards against subtle behaviour drift if a refactor
|
||||||
|
// reorders the random draws.
|
||||||
|
Random rngA = new Random(55L);
|
||||||
|
int base = OreDropMath.baseDropCount(4, 9, rngA);
|
||||||
|
int fortune = OreDropMath.applyFortune("lapis", base, true, 3, rngA);
|
||||||
|
|
||||||
|
Random rngB = new Random(55L);
|
||||||
|
int combined = OreDropMath.dropCount("lapis", 4, 9, true, 3, rngB);
|
||||||
|
|
||||||
|
assertEquals(fortune, combined, "dropCount must be base+fortune in order");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// experienceFor
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("XP: iron/gold/copper/unknown drop 0 raw XP")
|
||||||
|
void experienceFor_zeroXpTypes() {
|
||||||
|
Random rng = new Random(1L);
|
||||||
|
assertEquals(0, OreDropMath.experienceFor("impure_iron", rng));
|
||||||
|
assertEquals(0, OreDropMath.experienceFor("impure_gold", rng));
|
||||||
|
assertEquals(0, OreDropMath.experienceFor("pure_golden", rng));
|
||||||
|
assertEquals(0, OreDropMath.experienceFor("copper", rng));
|
||||||
|
assertEquals(0, OreDropMath.experienceFor("unknown", rng));
|
||||||
|
assertEquals(0, OreDropMath.experienceFor("", rng));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("XP ranges match vanilla 1.21 for each ore type")
|
||||||
|
void experienceFor_ranges() {
|
||||||
|
Random rng = new Random(2L);
|
||||||
|
// coal: 0-2
|
||||||
|
for (int i = 0; i < 5_000; i++) {
|
||||||
|
int xp = OreDropMath.experienceFor("concentrated_coal", rng);
|
||||||
|
assertTrue(xp >= 0 && xp <= 2, "coal XP " + xp + " out of [0,2]");
|
||||||
|
}
|
||||||
|
// diamond/emerald/shard: 3-7
|
||||||
|
for (String t : new String[]{"shard_diamond", "concentrated_diamond", "emerald"}) {
|
||||||
|
for (int i = 0; i < 5_000; i++) {
|
||||||
|
int xp = OreDropMath.experienceFor(t, rng);
|
||||||
|
assertTrue(xp >= 3 && xp <= 7, t + " XP " + xp + " out of [3,7]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// lapis: 2-5
|
||||||
|
for (int i = 0; i < 5_000; i++) {
|
||||||
|
int xp = OreDropMath.experienceFor("lapis", rng);
|
||||||
|
assertTrue(xp >= 2 && xp <= 5, "lapis XP " + xp + " out of [2,5]");
|
||||||
|
}
|
||||||
|
// redstone: 1-5
|
||||||
|
for (int i = 0; i < 5_000; i++) {
|
||||||
|
int xp = OreDropMath.experienceFor("redstone", rng);
|
||||||
|
assertTrue(xp >= 1 && xp <= 5, "redstone XP " + xp + " out of [1,5]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("XP reaches both bounds of its range over many draws")
|
||||||
|
void experienceFor_boundsHit() {
|
||||||
|
Random rng = new Random(3L);
|
||||||
|
boolean[] coalSeen = new boolean[3]; // 0,1,2
|
||||||
|
boolean[] redstoneSeen = new boolean[6]; // 1..5
|
||||||
|
boolean[] diamondSeen = new boolean[8]; // 3..7
|
||||||
|
for (int i = 0; i < 100_000; i++) {
|
||||||
|
coalSeen[OreDropMath.experienceFor("concentrated_coal", rng)] = true;
|
||||||
|
redstoneSeen[OreDropMath.experienceFor("redstone", rng)] = true;
|
||||||
|
diamondSeen[OreDropMath.experienceFor("emerald", rng)] = true;
|
||||||
|
}
|
||||||
|
for (int v = 0; v <= 2; v++) assertTrue(coalSeen[v], "coal XP " + v + " never observed");
|
||||||
|
for (int v = 1; v <= 5; v++) assertTrue(redstoneSeen[v], "redstone XP " + v + " never observed");
|
||||||
|
for (int v = 3; v <= 7; v++) assertTrue(diamondSeen[v], "emerald XP " + v + " never observed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Determinism: same seed -> same output
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Same RNG seed produces identical drop+XP sequences")
|
||||||
|
void determinism_sameSeedSameOutput() {
|
||||||
|
String[] oreTypes = {"shard_diamond", "lapis", "redstone", "emerald", "concentrated_coal",
|
||||||
|
"copper", "impure_iron", "impure_gold", "pure_golden", "concentrated_diamond"};
|
||||||
|
Random rngA = new Random(0xC0FFEE);
|
||||||
|
Random rngB = new Random(0xC0FFEE);
|
||||||
|
for (String t : oreTypes) {
|
||||||
|
int da = OreDropMath.dropCount(t, 2, 5, true, 3, rngA);
|
||||||
|
int db = OreDropMath.dropCount(t, 2, 5, true, 3, rngB);
|
||||||
|
assertEquals(da, db, t + " dropCount diverged with same seed");
|
||||||
|
int xa = OreDropMath.experienceFor(t, rngA);
|
||||||
|
int xb = OreDropMath.experienceFor(t, rngB);
|
||||||
|
assertEquals(xa, xb, t + " XP diverged with same seed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRatio(String label, double actual, double expected, double tol) {
|
||||||
|
assertTrue(Math.abs(actual - expected) <= tol,
|
||||||
|
label + ": expected ~" + expected + " ±" + tol + ", got " + actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user