WIP: ore nodes, catching rules, bee nest fix, smaller Deep Dark

Done and checked: surface and cave-floor ore nodes (companion mod
ore/OreNodes.kt), bigger and rarer underground veins; template with nodes
passed the world check and the island quick check (node self-tests).

Built, not yet tested: catching rules (catch rate falls with the level
gap to the trainer's strongest Pokemon, failed catch enrages, strong wild
Pokemon hunt weaker trainers; catching/, Fight or Flight config), bee
nests emptied when their bee is turned away.

In progress: the Deep Dark no longer fills the deepslate layer (split by
temperature: cold = Deep Dark, hot = basalt deltas, warm = ordinary caves;
world/biomes.json), zone layout regenerated; cave zone types added to
world/zones.json but tools/zone_spawns.py doesn't use them yet; template
not rebuilt for the new caves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-30 15:44:17 +01:00
1 parent 362d06aa4b
commit 6361636da7
29 files changed
+1726 -350

No files matched your search

+2 -2
View File
@@ -57,7 +57,7 @@ Only the companion mod's in-game tests, on a fresh flat world (faster while work
```bash ```bash
python3 tools/make_world.py python3 tools/make_world.py
``` ```
Builds the island world template (`build/world-template`, about 20 minutes: pregeneration, then a pass that waits until every chunk has its ore veins, then the showcase veins), runs the same world checks on the whole island plus border fit and End access, and renders `build/island-map.png`. Only needed when the island itself should change. Details in [world/README.md](world/README.md). Run `tools/prism_instance.py` afterwards to put it in Prism as the save "Cobblemon Survival Island". Builds the island world template (`build/world-template`, about 20 minutes: pregeneration, then a pass that waits until every chunk has its ore veins and nodes), runs the same world checks on the whole island plus border fit and End access, and renders `build/island-map.png`. Only needed when the island itself should change. Details in [world/README.md](world/README.md). Run `tools/prism_instance.py` afterwards to put it in Prism as the save "Cobblemon Survival Island".
```bash ```bash
python3 tools/zone_layout.py python3 tools/zone_layout.py
@@ -72,7 +72,7 @@ Writes each zone's spawn pool from Cobblemon's spawn data (every evolution line
```bash ```bash
python3 tools/zone_ores.py python3 tools/zone_ores.py
``` ```
Writes Regenerating Ore Veins' config (`pack/config/regenerating_ore_veins/`) from `world/zones.json` → `ores`: each ore tier's veins only in its zone, plus `world/vein-sources.json` for the field guide. Run after changing the ore tiers or the zone grid; the template must be rebuilt for new veins. Writes Regenerating Ore Veins' config (`pack/config/regenerating_ore_veins/`) and the companion mod's ore node table from `world/zones.json` → `ores`: each ore tier's surface or cave-floor nodes and underground veins only in its zone, plus `world/vein-sources.json` for the field guide. Run after changing the ore tiers or the zone grid; the template must be rebuilt for new veins.
```bash ```bash
python3 tools/drop_audit.py python3 tools/drop_audit.py
@@ -0,0 +1,11 @@
{
"_comment": "Catching (Adrian, 2026-10-30), measured against the trainer's strongest party Pokémon. catch_rate: full catch rate up to 'full_up_to_levels_above', then falling to 'minimum' (share of the normal rate) at 'minimum_at_levels_above' and beyond. failed_catch_enrages: a Pokémon that breaks out of a ball attacks the thrower instead of fleeing. hostile_when_stronger: a wild Pokémon above the strongest Pokémon in a player's party attacks that player when within 'hostile_range' blocks. Placeholders; see docs/balance.md. Read by the companion mod (catching/Catching.kt).",
"catch_rate": {
"full_up_to_levels_above": 0,
"minimum_at_levels_above": 20,
"minimum": 0.02
},
"failed_catch_enrages": true,
"hostile_when_stronger": true,
"hostile_range": 16
}
@@ -0,0 +1,194 @@
{
"_comment": "Generated by tools/zone_ores.py from world/zones.json; read by the companion mod (ore/OreNodes.kt).",
"nodes": [
{
"vein": "coal_node",
"zone": "button_isle",
"where": "surface",
"chance": 0.5,
"radius": 1
},
{
"vein": "copper_node",
"zone": "southwold",
"where": "surface",
"chance": 0.2,
"radius": 1
},
{
"vein": "iron_node",
"zone": "westmarch",
"where": "surface",
"chance": 0.2,
"radius": 1
},
{
"vein": "redstone_node",
"zone": "jungle",
"where": "surface",
"chance": 0.2,
"radius": 1
},
{
"vein": "lapis_node",
"zone": "highlands",
"where": "surface",
"chance": 0.15,
"radius": 1
},
{
"vein": "gold_node",
"zone": "sunscorch",
"where": "surface",
"chance": 0.3,
"radius": 1
},
{
"vein": "emerald_node",
"zone": "frostpeak",
"where": "surface",
"chance": 0.15,
"radius": 1
},
{
"vein": "amethyst_node",
"zone": "dripstone",
"where": "cave",
"chance": 0.5,
"radius": 1,
"biome": "minecraft:dripstone_caves",
"tries": 8,
"y": [
0,
40
]
},
{
"vein": "diamond_node",
"zone": "deep_dark",
"where": "cave",
"chance": 0.15,
"radius": 1,
"biome": "minecraft:deep_dark",
"tries": 8,
"y": [
-64,
-24
]
},
{
"vein": "quartz_node",
"zone": "basalt",
"where": "cave",
"chance": 0.5,
"radius": 1,
"biome": "minecraft:basalt_deltas",
"tries": 8,
"y": [
0,
40
]
},
{
"vein": "netherite_node",
"zone": "basalt",
"where": "cave",
"chance": 0.06,
"radius": 0,
"biome": "minecraft:basalt_deltas",
"tries": 8,
"y": [
0,
40
]
},
{
"vein": "thunder_stone_node",
"zone": "westmarch",
"where": "surface",
"chance": 0.03,
"radius": 0
},
{
"vein": "shiny_stone_node",
"zone": "lush_caves",
"where": "cave",
"chance": 0.15,
"radius": 1,
"biome": "minecraft:lush_caves",
"tries": 8,
"y": [
-16,
40
]
},
{
"vein": "leaf_stone_node",
"zone": "jungle",
"where": "surface",
"chance": 0.04,
"radius": 0
},
{
"vein": "water_stone_node",
"zone": "mire",
"where": "surface",
"chance": 0.04,
"radius": 0
},
{
"vein": "moon_stone_node",
"zone": "dripstone",
"where": "cave",
"chance": 0.25,
"radius": 1,
"biome": "minecraft:dripstone_caves",
"tries": 8,
"y": [
0,
40
]
},
{
"vein": "sun_stone_node",
"zone": "highlands",
"where": "surface",
"chance": 0.02,
"radius": 0
},
{
"vein": "fire_stone_node",
"zone": "sunscorch",
"where": "surface",
"chance": 0.08,
"radius": 0
},
{
"vein": "ice_stone_node",
"zone": "frostpeak",
"where": "surface",
"chance": 0.06,
"radius": 0
},
{
"vein": "dawn_stone_node",
"zone": "frostpeak",
"where": "surface",
"chance": 0.04,
"radius": 0
},
{
"vein": "dusk_stone_node",
"zone": "deep_dark",
"where": "cave",
"chance": 0.1,
"radius": 1,
"biome": "minecraft:deep_dark",
"tries": 8,
"y": [
-64,
-24
]
}
]
}
@@ -0,0 +1,23 @@
{
"values": [
"#minecraft:dirt",
"#minecraft:sand",
"#minecraft:base_stone_overworld",
"#minecraft:terracotta",
"minecraft:gravel",
"minecraft:clay",
"minecraft:snow_block",
"minecraft:packed_ice",
"minecraft:sandstone",
"minecraft:red_sandstone",
"minecraft:calcite",
"minecraft:dripstone_block",
"minecraft:sculk",
"minecraft:basalt",
"minecraft:smooth_basalt",
"minecraft:blackstone",
"minecraft:netherrack",
"minecraft:magma_block",
"minecraft:cobbled_deepslate"
]
}
@@ -7602,7 +7602,8 @@
{"biome":"minecraft:desert","parameters":{"continentalness":[-0.19,-0.11],"depth":1.0,"erosion":[0.55,1.0],"humidity":[0.3,1.0],"offset":0.0,"temperature":[0.55,1.0],"weirdness":[0.9333,1.0]}}, {"biome":"minecraft:desert","parameters":{"continentalness":[-0.19,-0.11],"depth":1.0,"erosion":[0.55,1.0],"humidity":[0.3,1.0],"offset":0.0,"temperature":[0.55,1.0],"weirdness":[0.9333,1.0]}},
{"biome":"minecraft:dripstone_caves","parameters":{"continentalness":[0.8,1.0],"depth":[0.2,0.9],"erosion":[-1.0,1.0],"humidity":[-1.0,1.0],"offset":0.0,"temperature":[-1.0,1.0],"weirdness":[-1.0,1.0]}}, {"biome":"minecraft:dripstone_caves","parameters":{"continentalness":[0.8,1.0],"depth":[0.2,0.9],"erosion":[-1.0,1.0],"humidity":[-1.0,1.0],"offset":0.0,"temperature":[-1.0,1.0],"weirdness":[-1.0,1.0]}},
{"biome":"minecraft:lush_caves","parameters":{"continentalness":[-1.0,1.0],"depth":[0.2,0.9],"erosion":[-1.0,1.0],"humidity":[0.7,1.0],"offset":0.0,"temperature":[-1.0,1.0],"weirdness":[-1.0,1.0]}}, {"biome":"minecraft:lush_caves","parameters":{"continentalness":[-1.0,1.0],"depth":[0.2,0.9],"erosion":[-1.0,1.0],"humidity":[0.7,1.0],"offset":0.0,"temperature":[-1.0,1.0],"weirdness":[-1.0,1.0]}},
{"biome":"minecraft:deep_dark","parameters":{"continentalness":[-1.0,1.0],"depth":1.1,"erosion":[-1.0,-0.375],"humidity":[-1.0,1.0],"offset":0.0,"temperature":[-1.0,1.0],"weirdness":[-1.0,1.0]}}, {"biome":"minecraft:deep_dark","parameters":{"continentalness":[-1.0,1.0],"depth":1.1,"erosion":[-1.0,-0.375],"humidity":[-1.0,1.0],"offset":0.0,"temperature":[-1.0,-0.1],"weirdness":[-1.0,1.0]}},
{"biome":"minecraft:basalt_deltas","parameters":{"continentalness":[-1.0,1.0],"depth":1.1,"erosion":[-1.0,-0.375],"humidity":[-1.0,1.0],"offset":0.0,"temperature":[0.4,1.0],"weirdness":[-1.0,1.0]}},
{"biome":"minecraft:basalt_deltas","parameters":{"temperature":[0.55,1.0],"humidity":[-1.0,-0.1],"continentalness":[-0.11,1.0],"erosion":[-1.0,1.0],"weirdness":[-1.0,1.0],"depth":[0.2,0.9],"offset":0.0}} {"biome":"minecraft:basalt_deltas","parameters":{"temperature":[0.55,1.0],"humidity":[-1.0,-0.1],"continentalness":[-0.11,1.0],"erosion":[-1.0,1.0],"weirdness":[-1.0,1.0],"depth":[0.2,0.9],"offset":0.0}}
] ]
} }
@@ -1 +1 @@
{"_comment":"Generated by tools/zone_layout.py from world/zones.json; do not edit by hand.","seed":"isle-26","radius":1700,"step":24,"size":142,"zones":[{"id":"button_isle","name":"Button Isle","levels":[2,10]},{"id":"frostpeak","name":"Frostpeak Range","levels":[40,55]},{"id":"sunscorch","name":"Sunscorch","levels":[30,45]},{"id":"mire","name":"Mangrove Mire","levels":[25,38]},{"id":"jungle","name":"Verdant Jungle","levels":[18,30]},{"id":"highlands","name":"Highland Meadows","levels":[25,35]},{"id":"westmarch","name":"Westmarch","levels":[15,25]},{"id":"southwold","name":"Southwold","levels":[8,18]},{"id":"shallows","name":"Shallows","levels":[5,20]},{"id":"deep_sea","name":"Deep Sea","levels":[30,50]}],"caves":[{"id":"lush_caves","name":"Lush Caves","levels":[12,28],"biome":"minecraft:lush_caves"},{"id":"dripstone","name":"Dripstone Depths","levels":[22,38],"biome":"minecraft:dripstone_caves"},{"id":"deep_dark","name":"Deep Dark","levels":[45,60],"biome":"minecraft:deep_dark"},{"id":"basalt","name":"Basalt Furnace","levels":[55,70],"biome":"minecraft:basalt_deltas"}],"level_pressure":{"grace_levels":5,"per_level":0.1,"minimum":0.1},"samples":{"button_isle":[-116,null,-92],"frostpeak":[628,null,-572],"sunscorch":[1372,null,-236],"mire":[652,null,1012],"jungle":[844,null,196],"highlands":[100,null,-1076],"westmarch":[-428,null,-1100],"southwold":[-116,null,1108],"shallows":[988,null,4],"deep_sea":[-1268,null,4],"lush_caves":[-1028,0,292],"dripstone":[940,40,-212],"deep_dark":[292,-52,-812],"basalt":[1276,0,508]},"biome_samples":{"frostpeak":[580,null,-524],"sunscorch":[-44,null,-452],"mire":[652,null,1012],"jungle":[844,null,196]},"grid_rle":[[9,771],[8,20],[9,117],[8,30],[9,109],[8,36],[9,103],[8,17],[5,1],[8,1],[5,5],[8,1],[5,3],[8,15],[9,97],[8,12],[5,1],[8,1],[5,22],[8,11],[9,87],[8,3],[9,5],[8,8],[6,5],[5,26],[1,1],[8,10],[9,82],[8,6],[9,3],[8,8],[6,6],[5,24],[1,3],[5,2],[8,10],[9,79],[8,8],[9,2],[8,6],[6,8],[5,31],[1,1],[8,9],[9,75],[8,15],[6,11],[5,21],[1,6],[5,7],[8,9],[9,71],[8,15],[6,1],[8,1],[6,11],[5,20],[1,9],[5,7],[8,9],[9,68],[8,14],[6,1],[8,1],[6,13],[5,19],[1,11],[5,9],[8,7],[9,65],[8,14],[6,1],[8,1],[6,15],[5,18],[1,11],[5,12],[8,7],[9,62],[8,13],[6,1],[8,1],[2,1],[6,16],[5,18],[1,9],[5,15],[8,7],[9,60],[8,12],[6,1],[8,1],[2,3],[6,16],[5,18],[1,6],[5,20],[8,7],[9,57],[8,11],[6,4],[2,2],[6,17],[5,46],[8,6],[9,54],[8,12],[6,5],[2,1],[6,19],[5,46],[8,6],[9,52],[8,11],[6,7],[2,1],[6,19],[5,25],[1,5],[5,18],[8,5],[9,50],[8,11],[6,27],[5,26],[1,6],[5,18],[8,5],[9,48],[8,12],[6,27],[5,24],[1,8],[5,19],[8,5],[9,46],[8,9],[2,1],[8,2],[2,2],[6,26],[5,24],[1,9],[5,19],[8,5],[9,44],[8,9],[2,6],[6,26],[5,24],[1,11],[5,18],[8,5],[9,42],[8,9],[2,7],[6,26],[5,23],[1,5],[5,1],[1,5],[5,20],[8,5],[9,41],[8,8],[2,8],[6,26],[5,23],[1,12],[5,20],[8,5],[9,40],[8,7],[2,8],[6,27],[5,23],[1,13],[5,20],[8,5],[9,40],[8,5],[2,10],[6,26],[5,26],[1,10],[5,21],[8,5],[9,38],[8,5],[2,9],[6,28],[5,27],[1,7],[5,24],[8,5],[9,36],[8,5],[2,9],[6,29],[5,28],[1,5],[5,26],[8,4],[9,35],[8,5],[6,2],[2,4],[6,33],[5,29],[1,3],[5,28],[8,4],[9,33],[8,5],[6,40],[5,61],[8,4],[9,32],[8,5],[6,40],[5,20],[1,1],[5,41],[8,4],[9,30],[8,5],[6,41],[5,16],[1,3],[5,1],[1,5],[5,37],[8,4],[9,29],[8,5],[6,42],[5,12],[1,18],[5,11],[1,1],[5,21],[8,4],[9,28],[8,4],[6,43],[5,12],[1,17],[5,3],[1,4],[5,2],[1,1],[5,2],[1,1],[5,22],[8,4],[9,26],[8,5],[6,43],[5,12],[1,18],[5,1],[1,5],[5,1],[1,3],[5,1],[1,1],[5,22],[8,4],[9,26],[8,4],[6,44],[5,12],[1,11],[5,1],[1,6],[5,1],[1,3],[5,2],[1,4],[5,25],[8,4],[9,24],[8,5],[6,43],[5,5],[1,1],[5,7],[1,13],[5,1],[1,2],[5,7],[1,2],[5,1],[1,2],[5,25],[8,4],[9,24],[8,5],[6,43],[5,4],[1,4],[5,5],[1,9],[5,2],[1,2],[5,9],[1,8],[5,23],[8,5],[9,22],[8,4],[2,3],[6,43],[5,1],[1,8],[5,3],[1,7],[5,4],[1,1],[5,9],[1,9],[5,24],[8,4],[9,22],[8,4],[2,3],[6,43],[5,1],[1,8],[5,4],[1,5],[5,3],[1,3],[5,9],[1,9],[5,25],[8,4],[9,20],[8,4],[2,4],[6,43],[1,9],[5,4],[1,10],[5,10],[1,9],[5,25],[8,4],[9,20],[8,4],[6,47],[1,10],[5,4],[1,3],[5,2],[1,3],[5,10],[1,10],[5,26],[8,4],[9,18],[8,4],[6,48],[1,10],[5,3],[2,1],[5,1],[2,2],[5,15],[1,9],[5,28],[8,2],[9,19],[8,4],[6,48],[1,9],[2,7],[5,16],[1,9],[5,4],[1,3],[5,21],[8,1],[9,19],[8,4],[6,48],[1,9],[2,8],[5,16],[1,9],[5,3],[1,4],[5,22],[8,1],[9,18],[8,4],[6,48],[1,7],[2,10],[5,16],[1,9],[5,3],[1,5],[5,20],[2,1],[8,1],[9,18],[8,3],[6,50],[1,5],[2,11],[5,16],[1,9],[5,3],[1,5],[5,19],[2,2],[8,2],[9,16],[8,4],[6,50],[5,2],[1,2],[2,13],[5,15],[1,10],[5,3],[1,4],[5,18],[2,3],[5,1],[8,1],[9,16],[8,3],[6,51],[5,3],[2,12],[1,4],[5,14],[1,10],[5,23],[2,4],[5,1],[8,2],[9,15],[8,3],[6,51],[5,3],[2,11],[1,5],[5,15],[1,9],[5,23],[2,5],[5,1],[8,2],[9,14],[8,3],[6,51],[5,3],[2,11],[1,1],[5,2],[1,3],[5,15],[1,10],[5,20],[2,6],[5,1],[8,3],[9,10],[8,1],[9,1],[8,4],[6,51],[5,3],[2,3],[9,3],[8,5],[1,1],[5,3],[1,1],[5,16],[1,12],[5,17],[2,7],[5,1],[8,4],[9,9],[8,5],[4,2],[6,50],[5,3],[2,1],[8,1],[9,5],[8,7],[5,1],[1,1],[5,16],[1,3],[5,5],[1,5],[5,15],[2,9],[8,6],[9,7],[8,5],[4,5],[6,47],[5,1],[8,1],[9,2],[8,1],[9,5],[8Line truncated {"_comment":"Generated by tools/zone_layout.py from world/zones.json; do not edit by hand.","seed":"isle-26","radius":1700,"step":24,"size":142,"zones":[{"id":"button_isle","name":"Button Isle","levels":[2,10]},{"id":"frostpeak","name":"Frostpeak Range","levels":[40,55]},{"id":"sunscorch","name":"Sunscorch","levels":[30,45]},{"id":"mire","name":"Mangrove Mire","levels":[25,38]},{"id":"jungle","name":"Verdant Jungle","levels":[18,30]},{"id":"highlands","name":"Highland Meadows","levels":[25,35]},{"id":"westmarch","name":"Westmarch","levels":[15,25]},{"id":"southwold","name":"Southwold","levels":[8,18]},{"id":"shallows","name":"Shallows","levels":[5,20]},{"id":"deep_sea","name":"Deep Sea","levels":[30,50]}],"caves":[{"id":"lush_caves","name":"Lush Caves","levels":[12,28],"biome":"minecraft:lush_caves"},{"id":"dripstone","name":"Dripstone Depths","levels":[22,38],"biome":"minecraft:dripstone_caves"},{"id":"deep_dark","name":"Deep Dark","levels":[45,60],"biome":"minecraft:deep_dark"},{"id":"basalt","name":"Basalt Furnace","levels":[55,70],"biome":"minecraft:basalt_deltas"}],"level_pressure":{"grace_levels":5,"per_level":0.1,"minimum":0.1},"samples":{"button_isle":[-116,null,-92],"frostpeak":[628,null,-572],"sunscorch":[1372,null,-236],"mire":[652,null,1012],"jungle":[844,null,196],"highlands":[100,null,-1076],"westmarch":[-428,null,-1100],"southwold":[-116,null,1108],"shallows":[1468,null,4],"deep_sea":[-1316,null,4],"lush_caves":[-1028,0,292],"dripstone":[940,40,-212],"deep_dark":[484,-52,-1124],"basalt":[1252,0,508]},"biome_samples":{"frostpeak":[580,null,-524],"sunscorch":[-44,null,-452],"mire":[652,null,1012],"jungle":[844,null,196]},"grid_rle":[[9,912],[8,22],[9,115],[8,32],[9,106],[8,40],[9,99],[8,18],[5,13],[8,15],[9,96],[8,12],[6,1],[5,22],[1,2],[8,12],[9,92],[8,9],[6,5],[5,27],[1,1],[8,10],[9,90],[8,8],[6,6],[5,29],[1,1],[8,10],[9,85],[8,7],[6,10],[5,21],[1,3],[5,7],[1,1],[5,1],[8,10],[9,80],[8,6],[6,1],[8,1],[6,11],[5,20],[1,8],[5,2],[1,2],[5,2],[8,11],[9,76],[8,5],[6,2],[8,1],[6,13],[5,19],[1,11],[5,5],[8,1],[5,2],[8,8],[9,73],[8,5],[6,2],[8,1],[6,15],[5,18],[1,11],[5,12],[8,7],[9,60],[8,2],[9,7],[8,5],[6,1],[8,2],[6,17],[5,18],[1,9],[5,15],[8,8],[9,57],[8,3],[9,5],[8,5],[6,2],[2,4],[6,16],[5,18],[1,6],[5,20],[8,7],[9,55],[8,12],[6,5],[2,2],[6,17],[5,46],[8,7],[9,52],[8,12],[6,6],[2,1],[6,19],[5,47],[8,6],[9,49],[8,12],[6,8],[2,1],[6,19],[5,25],[1,5],[5,18],[8,6],[9,47],[8,11],[6,1],[8,1],[6,27],[5,26],[1,6],[5,19],[8,5],[9,45],[8,10],[2,1],[8,2],[2,1],[6,27],[5,24],[1,8],[5,20],[8,6],[9,42],[8,9],[2,7],[6,26],[5,24],[1,9],[5,20],[8,6],[9,40],[8,9],[2,8],[6,26],[5,24],[1,11],[5,19],[8,6],[9,39],[8,8],[2,9],[6,26],[5,23],[1,5],[5,1],[1,5],[5,21],[8,5],[9,40],[8,6],[2,10],[6,26],[5,23],[1,12],[5,21],[8,5],[9,39],[8,5],[2,10],[6,27],[5,23],[1,13],[5,21],[8,5],[9,37],[8,5],[2,12],[6,26],[5,26],[1,10],[5,22],[8,5],[9,35],[8,5],[2,11],[6,28],[5,27],[1,7],[5,25],[8,5],[9,34],[8,5],[2,10],[6,29],[5,28],[1,5],[5,27],[8,5],[9,32],[8,5],[2,1],[6,2],[2,4],[6,33],[5,29],[1,3],[5,29],[8,4],[9,31],[8,5],[6,41],[5,62],[8,4],[9,30],[8,4],[6,42],[5,20],[1,1],[5,42],[8,4],[9,28],[8,5],[6,42],[5,16],[1,3],[5,1],[1,5],[5,38],[8,4],[9,27],[8,5],[6,43],[5,12],[1,18],[5,11],[1,1],[5,22],[8,4],[9,26],[8,5],[6,43],[5,12],[1,17],[5,3],[1,4],[5,2],[1,1],[5,2],[1,1],[5,22],[8,4],[9,25],[8,5],[6,44],[5,12],[1,18],[5,1],[1,5],[5,1],[1,3],[5,1],[1,1],[5,23],[8,4],[9,24],[8,4],[6,45],[5,12],[1,11],[5,1],[1,6],[5,1],[1,3],[5,2],[1,4],[5,25],[8,4],[9,23],[8,5],[6,44],[5,5],[1,1],[5,7],[1,13],[5,1],[1,2],[5,7],[1,2],[5,1],[1,2],[5,26],[8,4],[9,22],[8,4],[6,45],[5,4],[1,4],[5,5],[1,9],[5,2],[1,2],[5,9],[1,8],[5,24],[8,4],[9,21],[8,4],[2,3],[6,44],[5,1],[1,8],[5,3],[1,7],[5,4],[1,1],[5,9],[1,9],[5,24],[8,5],[9,20],[8,4],[2,4],[6,43],[5,1],[1,8],[5,4],[1,5],[5,3],[1,3],[5,9],[1,9],[5,25],[8,4],[9,19],[8,4],[2,5],[6,43],[1,9],[5,4],[1,10],[5,10],[1,9],[5,25],[8,5],[9,1],[8,1],[9,16],[8,4],[6,48],[1,10],[5,4],[1,3],[5,2],[1,3],[5,10],[1,10],[5,26],[8,6],[9,16],[8,4],[6,48],[1,10],[5,3],[2,1],[5,1],[2,2],[5,15],[1,9],[5,27],[8,7],[9,14],[8,4],[6,49],[1,9],[2,7],[5,16],[1,9],[5,4],[1,3],[5,21],[8,6],[9,14],[8,4],[6,48],[1,9],[2,8],[5,16],[1,9],[5,3],[1,4],[5,21],[8,6],[9,14],[8,3],[6,49],[1,7],[2,10],[5,16],[1,9],[5,3],[1,5],[5,20],[2,1],[8,6],[9,12],[8,4],[6,50],[1,5],[2,11],[5,16],[1,9],[5,3],[1,5],[5,19],[2,2],[8,6],[9,12],[8,4],[6,50],[5,2],[1,2],[2,13],[5,15],[1,10],[5,3],[1,4],[5,18],[2,3],[8,6],[9,12],[8,3],[6,51],[5,3],[2,12],[1,4],[5,14],[1,10],[5,23],[2,4],[8,7],[9,10],[8,4],[6,51],[5,3],[2,11],[1,5],[5,15],[1,9],[5,23],[2,5],[8,6],[9,10],[8,3],[6,52],[5,3],[2,11],[1,1],[5,2],[1,3],[5,15],[1,10],[5,20],[2,6],[8,6],[9,10],[8,3],[6,52],[5,3],[2,3],[8,3],[9,3],[8,2],[1,1],[5,3],[1,1],[5,16],[1,12],[5,17],[2,7],[8,6],[9,12],[8,1],[4,2],[6,50],[5,3],[2,1],[8,6],[9,2],[8,5],[5,1],[1,1],[5,16],[1,3],[5,5],[1,5],[5,15],[2,8],[5,1],[8,1],[9,1],[8,3],[9,7],[8,3],[9,2],[8,1],[4,5],[6,47],[5,1],[8,1],[9,2],[8,6],[9,3],[8,6],[5,6],[4,1],[5,9],[Line truncated
+5 -1
View File
@@ -32,7 +32,8 @@ Every tunable lives in a config file or in this table. **Placeholder** means gue
| High zone for strong flyers | band starts at Lv 30 or more (Sunscorch, Frostpeak, Deep Sea, Deep Dark, Basalt Furnace) | Decided (Adrian, 2026-10-30) | `tools/zone_spawns.py` (`HIGH_ZONE_LEVEL`) | | | High zone for strong flyers | band starts at Lv 30 or more (Sunscorch, Frostpeak, Deep Sea, Deep Dark, Basalt Furnace) | Decided (Adrian, 2026-10-30) | `tools/zone_spawns.py` (`HIGH_ZONE_LEVEL`) | |
| Ore tiers per zone | Button Isle: coal; Southwold: copper; Westmarch: iron, thunder stone; Verdant Jungle: redstone, leaf stone; Highland Meadows: lapis, sun stone; Sunscorch: gold, fire stone; Frostpeak Range: emerald, ice stone, dawn stone; Dripstone Depths: amethyst, moon stone; Deep Dark: diamond, dusk stone; Basalt Furnace: quartz, netherite; Lush Caves: shiny stone; Mangrove Mire: water stone | Fine for now (Adrian, 2026-10-30) | `world/zones.json` → `ores` → `tools/zone_ores.py` | Each tier in exactly one zone; evolution stones are tiers (their natural ores are off) | | Ore tiers per zone | Button Isle: coal; Southwold: copper; Westmarch: iron, thunder stone; Verdant Jungle: redstone, leaf stone; Highland Meadows: lapis, sun stone; Sunscorch: gold, fire stone; Frostpeak Range: emerald, ice stone, dawn stone; Dripstone Depths: amethyst, moon stone; Deep Dark: diamond, dusk stone; Basalt Furnace: quartz, netherite; Lush Caves: shiny stone; Mangrove Mire: water stone | Fine for now (Adrian, 2026-10-30) | `world/zones.json` → `ores` → `tools/zone_ores.py` | Each tier in exactly one zone; evolution stones are tiers (their natural ores are off) |
| Vein regrowth | 20 min: coal; 30 min: copper; 40 min: iron; 45 min: redstone, lapis; 60 min: gold, quartz, thunder stone, shiny stone, leaf stone, water stone, moon stone, sun stone, fire stone, ice stone; 75 min: emerald, dawn stone, dusk stone; 90 min: diamond; 120 min: amethyst; 180 min: netherite; ±10 % jitter; real time | Fine for now (Adrian, 2026-10-30) | `world/zones.json` → `ores` (`regen_minutes`, `ore_defaults.jitter`) | Counts while the server is off too | | Vein regrowth | 20 min: coal; 30 min: copper; 40 min: iron; 45 min: redstone, lapis; 60 min: gold, quartz, thunder stone, shiny stone, leaf stone, water stone, moon stone, sun stone, fire stone, ice stone; 75 min: emerald, dawn stone, dusk stone; 90 min: diamond; 120 min: amethyst; 180 min: netherite; ±10 % jitter; real time | Fine for now (Adrian, 2026-10-30) | `world/zones.json` → `ores` (`regen_minutes`, `ore_defaults.jitter`) | Counts while the server is off too |
| Vein spacing, size, tries | spacing 4–14 chunks between candidate chunks, radius 0–2, 70 % filled, 1–4 tries per candidate chunk (per tier in `world/zones.json`) | Fine for now (Adrian, 2026-10-30) | `world/zones.json` → `ores`, `ore_defaults` | Ore blocks in the template (2026-10-30): copper 10,400, iron 6,356, quartz 2,397, lapis 2,236, coal 2,222, redstone 1,044, gold 815, diamond 559, shiny stone 466, thunder stone 239, sun stone 238, amethyst 238, dusk stone 222, emerald 181, leaf stone 125, ice stone 102, moon stone 100, fire stone 98, water stone 95, netherite 74, dawn stone 64. Per zone in `build/island-report.json` after `tools/make_world.py` | | Ore nodes | chance per chunk of the zone — surface: coal 50 %, copper 20 %, iron 20 %, redstone 20 %, lapis 15 %, gold 30 %, emerald 15 %, thunder stone 3 %, leaf stone 4 %, water stone 4 %, sun stone 2 %, fire stone 8 %, ice stone 6 %, dawn stone 4 %; cave floor (8 columns searched): amethyst 50 %, diamond 15 %, quartz 50 %, netherite 6 %, shiny stone 15 %, moon stone 25 %, dusk stone 10 %. Radius 0–1 (evolution stones and netherite one block), 80 % filled | Placeholder (Adrian, 2026-10-30: small on the surface) | `world/zones.json` → `ores` → `nodes` | |
| Underground veins (surface tiers) | radius 2–4 (stones 1–2), spacing 6–11 chunks between candidate chunks, 70 % filled, 1–2 tries per candidate chunk | Placeholder (Adrian, 2026-10-30: larger but harder to find) | `world/zones.json` → `ores` → `veins` | Ore blocks in the template (2026-10-30): copper 10,400, iron 6,356, quartz 2,397, lapis 2,236, coal 2,222, redstone 1,044, gold 815, diamond 559, shiny stone 466, thunder stone 239, sun stone 238, amethyst 238, dusk stone 222, emerald 181, leaf stone 125, ice stone 102, moon stone 100, fire stone 98, water stone 95, netherite 74, dawn stone 64. Per zone in `build/island-report.json` after `tools/make_world.py` |
| Regenerators breakable by players | no; explosions don't hurt veins | Fine for now (Adrian, 2026-10-30) | `tools/zone_ores.py` → `global.json` | Otherwise a player could remove a vein for good | | Regenerators breakable by players | no; explosions don't hurt veins | Fine for now (Adrian, 2026-10-30) | `tools/zone_ores.py` → `global.json` | Otherwise a player could remove a vein for good |
| Vein Locator | compass + copper ingot; 75 % chance to break per use (the mod's default) | Fine for now (Adrian, 2026-10-30) | `tools/zone_ores.py` → `vein_locator.json` | The mod's default recipe needs netherite | | Vein Locator | compass + copper ingot; 75 % chance to break per use (the mod's default) | Fine for now (Adrian, 2026-10-30) | `tools/zone_ores.py` → `vein_locator.json` | The mod's default recipe needs netherite |
| Drops for mobs the island lacks | Blaze Rod: Carkol 10 %, Torkoal 25 % ×1–2; Ghast Tear: Drifloon 5 %, Dusclops 10 %; Breeze Rod: Whimsicott 10 %; Shulker Shell: Shuckle 10 %, Cloyster 10 %; Nether Star: Charizard 5 % | Fine for now (Adrian, 2026-10-30) | `datapacks/core/data/cobblesurvival/species_additions/` | Every drop and what consumes it: `docs/pokemon-drops.md` (`tools/drop_audit.py`) | | Drops for mobs the island lacks | Blaze Rod: Carkol 10 %, Torkoal 25 % ×1–2; Ghast Tear: Drifloon 5 %, Dusclops 10 %; Breeze Rod: Whimsicott 10 %; Shulker Shell: Shuckle 10 %, Cloyster 10 %; Nether Star: Charizard 5 % | Fine for now (Adrian, 2026-10-30) | `datapacks/core/data/cobblesurvival/species_additions/` | Every drop and what consumes it: `docs/pokemon-drops.md` (`tools/drop_audit.py`) |
@@ -40,6 +41,9 @@ Every tunable lives in a config file or in this table. **Placeholder** means gue
| Power Block gate | after the Verdant Jungle guardian | Placeholder | same file, `blocks` | Recipe: Poke Power's own (iron blocks, redstone blocks, Electirizer) | | Power Block gate | after the Verdant Jungle guardian | Placeholder | same file, `blocks` | Recipe: Poke Power's own (iron blocks, redstone blocks, Electirizer) |
| Poke Power output | 50 FE/t per Pokémon level, block stores 120,000 FE, 16,000 FE/t out; cable 32,000 / 16,000 | The mod's defaults | `pack/config/poke_power/server.toml` | Nothing uses the energy before Phase 6 | | Poke Power output | 50 FE/t per Pokémon level, block stores 120,000 FE, 16,000 FE/t out; cable 32,000 / 16,000 | The mod's defaults | `pack/config/poke_power/server.toml` | Nothing uses the energy before Phase 6 |
| Cobblebase jobs | the mod's defaults (finder cooldown 240 s, passive XP 5 % a minute with a Mentor, recruiter rates, buffs) | The mod's defaults | `pack/config/cobblebase.json` (skill cooldowns in its own skill data) | Tune after play-testing | | Cobblebase jobs | the mod's defaults (finder cooldown 240 s, passive XP 5 % a minute with a Mentor, recruiter rates, buffs) | The mod's defaults | `pack/config/cobblebase.json` (skill cooldowns in its own skill data) | Tune after play-testing |
| Catch rate against stronger Pokémon | full up to the trainer's strongest party level, then falling linearly to 2 % of the normal rate at +20 levels and beyond | Placeholder (Adrian, 2026-10-30: 20+ levels higher very improbable) | `datapacks/core/data/cobblesurvival/catching/rules.json` → `catch_rate` | E.g. +10 levels: about half the normal rate |
| Failed catch | the Pokémon attacks the thrower; while it has a target, thrown balls don't catch it | Decided (Adrian, 2026-10-30) | same file, `failed_catch_enrages`; `config/fightorflight.json5` | Pokémon below Lv 5 never fight back (Fight or Flight `minimum_attack_level`) |
| Hostile to weaker trainers | a wild Pokémon above a player's strongest party Pokémon attacks that player within 16 blocks | Decided (Adrian, 2026-10-30); range placeholder | same file, `hostile_when_stronger`, `hostile_range` | |
| Ride stats (speed, stamina, ...) | Cobblemon's own | To tune after play-testing | species `riding` via `species_additions` | | | Ride stats (speed, stamina, ...) | Cobblemon's own | To tune after play-testing | species `riding` via `species_additions` | |
| Healing while sleeping | off (`healPercent` 0) | Decided (roadmap) | `config/cobblemon/main.json` | | | Healing while sleeping | off (`healPercent` 0) | Decided (roadmap) | `config/cobblemon/main.json` | |
| Loot container refresh | 1 hour (72,000 ticks of game time) after first opened, all containers | Decided (Adrian, 2026-10-29) | `config/lootr-common.toml` | Tiering by structure/biome later | | Loot container refresh | 1 hour (72,000 ticks of game time) after first opened, all containers | Decided (Adrian, 2026-10-29) | `config/lootr-common.toml` | Tiering by structure/biome later |
+3 -2
View File
@@ -3,7 +3,7 @@
## Built so far ## Built so far
- **Ore veins** (Regenerating Ore Veins). Ore only comes from veins that grow back (and from chest loot). Each of the 21 ore tiers has veins in exactly one zone; Cobblemon's evolution stones are tiers too (`docs/balance.md` → "Ore tiers per zone"). Automated: the world report on the template (ore only in tracked veins inside their tier's zone, every tier present, every chunk filled) and the self-tests `vein_regrows_after_unload` and `vein_regrows_after_restart`. - **Ore veins** (Regenerating Ore Veins). Ore only comes from veins that grow back (and from chest loot). Each of the 21 ore tiers has veins in exactly one zone; Cobblemon's evolution stones are tiers too (`docs/balance.md` → "Ore tiers per zone"). Automated: the world report on the template (ore only in tracked veins inside their tier's zone, every tier present, every chunk filled) and the self-tests `vein_regrows_after_unload` and `vein_regrows_after_restart`.
- **Showcase veins.** One per surface zone sticks out of the ground near the zone's middle; on Button Isle at about −100, ~106, −92, a short walk from spawn. - **Ore nodes** (Adrian, 2026-10-30): small nodes on the surface, larger veins underground that are harder to find, and the cave zones' ores as nodes on the cave floor. Automated: `surface_nodes_lie_in_the_ground`, `cave_nodes_lie_on_the_cave_floor`, and the world report (every tier has nodes).
- **Drops for mobs the island lacks.** Blaze rods, ghast tears, breeze rods, shulker shells and nether stars now drop from Pokémon; every drop and what consumes it is in `docs/pokemon-drops.md` (`tools/drop_audit.py` fails if a crafting input has no source). Automated: `drop_additions_apply`. - **Drops for mobs the island lacks.** Blaze rods, ghast tears, breeze rods, shulker shells and nether stars now drop from Pokémon; every drop and what consumes it is in `docs/pokemon-drops.md` (`tools/drop_audit.py` fails if a crafting input has no source). Automated: `drop_additions_apply`.
## In game (Prism instance) ## In game (Prism instance)
@@ -11,7 +11,8 @@
The new template is the save folder "Cobblemon Survival Island (Phase 4)"; the world list shows it as "Cobblemon Survival Island" too, the newer of the two. The save you played on 28 Oct is untouched (veins only grow in new chunks, so it has almost no ore). The new template is the save folder "Cobblemon Survival Island (Phase 4)"; the world list shows it as "Cobblemon Survival Island" too, the newer of the two. The save you played on 28 Oct is untouched (veins only grow in new chunks, so it has almost no ore).
- [ ] Walk to the Button Isle showcase vein (about −100, ~106, −92), mine a coal block: it turns into a regenerator block, and right-clicking it says how long until it grows back (20 minutes of real time for coal). - [ ] Walk around Button Isle: small coal nodes lie in the grass. Mine one: it turns into a regenerator block, and right-clicking it says how long until it grows back (20 minutes of real time for coal).
- [ ] Fly through a cave zone in spectator mode: its nodes sit on the cave floor (amethyst in the Dripstone Depths, diamonds in the Deep Dark, ...), not inside the rock.
- [ ] The regenerator can't be broken in survival (pickaxe or not), and TNT next to a vein leaves the vein alone. - [ ] The regenerator can't be broken in survival (pickaxe or not), and TNT next to a vein leaves the vein alone.
- [ ] Craft a Vein Locator (compass + copper ingot) and use it underground in Westmarch: it should point at iron. - [ ] Craft a Vein Locator (compass + copper ingot) and use it underground in Westmarch: it should point at iron.
- [ ] Dig around under a zone: only that zone's ores show up (e.g. no iron under Southwold, only copper). - [ ] Dig around under a zone: only that zone's ores show up (e.g. no iron under Southwold, only copper).
+18 -6
View File
@@ -132,15 +132,17 @@ Order of play the bands suggest: Button Isle → Southwold → Westmarch or Jung
**Adrian's answers (2026-10-30):** Cobblebase's skills look fun and stay in, gated behind progression; power comes from an existing mod, also gated (Cobblemon Poke Power); evolution stones as ores are fine for now; the balance values are fine for now and stay configurable, to tweak later. **Adrian's answers (2026-10-30):** Cobblebase's skills look fun and stay in, gated behind progression; power comes from an existing mod, also gated (Cobblemon Poke Power); evolution stones as ores are fine for now; the balance values are fine for now and stay configurable, to tweak later.
**Ore veins.** Ore only comes from [Regenerating Ore Veins](https://www.curseforge.com/minecraft/mc-mods/regenerating-ore-veins) (plus chest loot): a mined vein block turns into a regenerator and grows back after its time, which counts in real time, unlike the guardians' game time. Each ore tier has veins in exactly one zone, higher tiers in higher zones (table in `docs/balance.md`). `tools/zone_ores.py` writes the mod's config (`pack/config/regenerating_ore_veins/`) from `world/zones.json` → `ores`: **Ore nodes and veins.** Ore only comes from [Regenerating Ore Veins](https://www.curseforge.com/minecraft/mc-mods/regenerating-ore-veins) (plus chest loot): a mined ore block turns into a regenerator and grows back after its time, which counts in real time, unlike the guardians' game time. Each ore tier lives in exactly one zone, higher tiers in higher zones (table in `docs/balance.md`). Adrian (2026-10-30): ore shows up mainly as **small nodes on the surface**, the **larger veins underground are harder to find**, and the cave zones' ores are **nodes on the cave floor, not in the walls**. `tools/zone_ores.py` writes the vein mod's config (`pack/config/regenerating_ore_veins/`) and the node table from `world/zones.json` → `ores`:
- A surface zone is a set of boxes cut from the zone grid; the mod checks every block of a vein against them, so no vein block lies outside its zone. Cave biomes are zones of their own, so surface tiers stay out of them; the mod checks the biome only at a vein's centre, so a vein's edge can reach up to two blocks over a cave border. Cave tiers are limited to their cave biome and the depths where it lies (the Deep Dark mostly below y −24, the others between 0 and 40). - **Surface nodes** (every surface tier): 1–5 ore blocks lying in natural ground (grass, dirt, sand, stone, snow, ...; tag `cobblesurvival:ore_node_ground`), at most one block standing out, never in water or under leaves, a given chance per chunk of the tier's zone. Evolution stones are single blocks and rarer.
- Tiers with an ordinary and a deepslate ore get two veins: the ordinary ore from y 0 up, the deepslate one below. - **Cave nodes** (Lush Caves, Dripstone Depths, Deep Dark, Basalt Furnace): a lump on a cave floor in the tier's cave biome, at the depths where that biome lies (the Deep Dark mostly below y −24, the others between 0 and 40). Cave tiers have no veins inside the rock.
- The companion mod places the nodes (`ore/OreNodes.kt`, table `datapacks/core/data/cobblesurvival/ore_nodes/island.json`) in every new overworld chunk, through the vein mod's own placement, so they are veins like any other and grow back.
- **Underground veins** (surface tiers): larger (radius 2–4) and fewer than the nodes, generated by the vein mod inside the rock under their zone. A surface zone is a set of boxes cut from the zone grid; the mod checks every vein block against them, so no vein block lies outside its zone; cave biomes are zones of their own, which the veins keep out of (the mod checks the biome at a vein's centre, so an edge can reach a little over a cave border). Tiers with an ordinary and a deepslate ore get two veins: the ordinary ore from y 0 up, the deepslate one below.
- **Vein Locator**: attuned in the crafting grid with an ore *block* (needs Silk Touch), it finds the nearest vein or node of exactly that block and throws a signal toward it like an Eye of Ender; 75 % chance to break per use. Attuned to the deepslate ore it only finds the deep half of the veins.
- **Cobblemon's evolution stones are tiers too** (approved for now, 2026-10-30): their natural ores have been off since Phase 1, so stones had no source. Each stone lives in one zone that fits it (Leaf Stone in the jungle, Water Stone in the Mire, Ice and Dawn Stone on Frostpeak, ...). - **Cobblemon's evolution stones are tiers too** (approved for now, 2026-10-30): their natural ores have been off since Phase 1, so stones had no source. Each stone lives in one zone that fits it (Leaf Stone in the jungle, Water Stone in the Mire, Ice and Dawn Stone on Frostpeak, ...).
- Players can't break regenerators (`allow_breaking` false), so nobody removes a vein for good, and explosions don't hurt veins. The Vein Locator is crafted from a compass and a copper ingot instead of netherite. - Players can't break regenerators (`allow_breaking` false), so nobody removes a vein for good, and explosions don't hurt veins. The Vein Locator is crafted from a compass and a copper ingot instead of netherite.
- The mod fills a new chunk with veins only while the chunk is loaded, 8 chunks a tick, and forgets the rest when the server stops. Pregeneration is faster than that, so `tools/make_world.py` loads the island again row by row until every chunk is filled (`/cobblesurvival veins unfilled` counts what's left), and the world report fails on any unfilled chunk. On a live server new chunks fill as players load them. - Both mods fill a new chunk only while it is loaded, a few chunks a tick, and the vein mod forgets the rest when the server stops. Pregeneration is faster than that, so `tools/make_world.py` loads the island again row by row until every chunk has its veins and nodes (`/cobblesurvival veins unfilled` counts what's left), and the world report fails on any chunk without them. On a live server new chunks fill as players load them.
- One showcase vein per surface zone (its first tier) sticks out of the ground near the zone's sample point, so players see what a vein looks like before they dig. - Checked: the world report (ore only in tracked veins and nodes inside their tier's zone, every tier with nodes and every surface tier with veins, every chunk filled) and the self-tests `surface_nodes_lie_in_the_ground`, `cave_nodes_lie_on_the_cave_floor`, `vein_regrows_after_unload` and `vein_regrows_after_restart` (a second server start in `tools/selftest.py` and `tools/quick_check.py`).
- Checked: the world report (ore only in tracked veins inside their tier's zone, every tier present, every chunk filled) and the self-tests `vein_regrows_after_unload` and `vein_regrows_after_restart` (a second server start in `tools/selftest.py` and `tools/quick_check.py`).
**Unique drops.** Only Pokémon live on the island, so a mob's drop needs another source. `tools/drop_audit.py` checks every item a Pokémon or a vanilla mob drops that a recipe or brewing consumes, and writes `docs/pokemon-drops.md` (item, wild Pokémon with zone and chance, other sources, consumers). Gaps it found, filled through Cobblemon species additions (`datapacks/core/data/cobblesurvival/species_additions/`, placeholders): Blaze Rod (Carkol in Westmarch, Torkoal in the Basalt Furnace; needed for brewing stands, blaze powder and Eyes of Ender), Ghast Tear (Drifloon, Dusclops), Breeze Rod (Whimsicott), Shulker Shell (Shuckle, Cloyster), Nether Star (Charizard). The tide armour trim and the wither skeleton skull stay without a source (cosmetic here). A zone spawn with its own drops overrides the species' drops, so the audit also fails if an addition targets such a spawn (Magmortar's and Centiskorch's did). Self-test: `drop_additions_apply`. **Unique drops.** Only Pokémon live on the island, so a mob's drop needs another source. `tools/drop_audit.py` checks every item a Pokémon or a vanilla mob drops that a recipe or brewing consumes, and writes `docs/pokemon-drops.md` (item, wild Pokémon with zone and chance, other sources, consumers). Gaps it found, filled through Cobblemon species additions (`datapacks/core/data/cobblesurvival/species_additions/`, placeholders): Blaze Rod (Carkol in Westmarch, Torkoal in the Basalt Furnace; needed for brewing stands, blaze powder and Eyes of Ender), Ghast Tear (Drifloon, Dusclops), Breeze Rod (Whimsicott), Shulker Shell (Shuckle, Cloyster), Nether Star (Charizard). The tide armour trim and the wither skeleton skull stay without a source (cosmetic here). A zone spawn with its own drops overrides the species' drops, so the audit also fails if an addition targets such a spawn (Magmortar's and Centiskorch's did). Self-test: `drop_additions_apply`.
@@ -151,6 +153,16 @@ Order of play the bands suggest: Button Isle → Southwold → Westmarch or Jung
- Operators: `/cobblesurvival progress <player>`, `... grant|revoke <player> <zone>`. - Operators: `/cobblesurvival progress <player>`, `... grant|revoke <player> <zone>`.
- Self-tests: `gates_cover_every_cobblebase_skill` (every skill has a gate, every gate zone has a guardian, every executor is gated), `cobblebase_skills_wait_for_guardians` and `power_block_waits_for_guardian`; `guardian_appears_and_returns` checks that the win counts as progress. - Self-tests: `gates_cover_every_cobblebase_skill` (every skill has a gate, every gate zone has a guardian, every executor is gated), `cobblebase_skills_wait_for_guardians` and `power_block_waits_for_guardian`; `guardian_appears_and_returns` checks that the win counts as progress.
**Catching high-level Pokémon** (Adrian, 2026-10-30). Measured against the trainer's strongest party Pokémon (`catching/Catching.kt`, values in `datapacks/core/data/cobblesurvival/catching/rules.json`):
- The catch rate falls with the level gap: full at the trainer's level, then linearly down to 2 % of the normal rate at 20 levels above and beyond ("20+ levels higher should be very improbable"). Applied to the rate Cobblemon's catch formula uses (its catch-rate event), so balls, status and HP still count.
- A failed catch angers the Pokémon: it attacks the thrower instead of fleeing.
- A wild Pokémon above the strongest Pokémon in a nearby player's party (16 blocks) is hostile to that player.
- The fighting is [Fight or Flight Reborn](https://modrinth.com/mod/cobblemon-fight-or-flight-reborn)'s: an angry Pokémon gets the aspect `cobblesurvival_enraged`, which the pack's `config/fightorflight.json5` lists in `always_aggro_aspects` (so it fights rather than flees), and its target is set through Fight or Flight's own "caught by" mark, by reflection. While an angry Pokémon has a target, thrown balls don't catch it (Fight or Flight's `aggressive_pokemon_catchable` false, its default): it has to be battled. Pokémon below Lv 5 never fight back (its `minimum_attack_level`).
- Self-tests: `catch_rate_falls_with_level_gap`, `failed_catch_enrages`, `strong_wild_pokemon_hunt_weak_trainers`, `wild_pokemon_ignore_stronger_trainers`.
**Bee nests** (found by Adrian in the Phase 4 world): a nest whose bee the island turns away kept the bee and tried to let it out every tick, with its sound each time. The companion mod now empties such a nest (`world/OnlyPokemon.kt`); honey still comes from Combee and shearing nests that have honey.
The field guide learned two things on the way: vanilla mobs count as absent (the companion mod lets only Cobblemon's entities join a world), and species additions replace the drops they set. The veins come in as sources from `world/vein-sources.json`. The field guide learned two things on the way: vanilla mobs count as absent (the companion mod lets only Cobblemon's entities join a world), and species additions replace the drops they set. The veins come in as sources from `world/vein-sources.json`.
## Open decisions for Adrian (proceeding with the default, flagged) ## Open decisions for Adrian (proceeding with the default, flagged)
@@ -3,8 +3,10 @@ package dev.cobblesurvival
import com.cobblemon.mod.common.Cobblemon import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.mojang.logging.LogUtils import com.mojang.logging.LogUtils
import dev.cobblesurvival.catching.Catching
import dev.cobblesurvival.dev.BiomePreview import dev.cobblesurvival.dev.BiomePreview
import dev.cobblesurvival.dev.OreVeins import dev.cobblesurvival.dev.OreVeins
import dev.cobblesurvival.ore.OreNodes
import dev.cobblesurvival.permadeath.Permadeath import dev.cobblesurvival.permadeath.Permadeath
import dev.cobblesurvival.progress.Gates import dev.cobblesurvival.progress.Gates
import dev.cobblesurvival.progress.Progress import dev.cobblesurvival.progress.Progress
@@ -58,11 +60,14 @@ class CobbleSurvival {
NeoForge.EVENT_BUS.addListener(::onPortalSpawn) NeoForge.EVENT_BUS.addListener(::onPortalSpawn)
NeoForge.EVENT_BUS.addListener(OnlyPokemon::onChunkLoad) NeoForge.EVENT_BUS.addListener(OnlyPokemon::onChunkLoad)
NeoForge.EVENT_BUS.addListener(OnlyPokemon::onEntityJoin) NeoForge.EVENT_BUS.addListener(OnlyPokemon::onEntityJoin)
NeoForge.EVENT_BUS.addListener(OnlyPokemon::onServerTick)
Permadeath.register() Permadeath.register()
ZoneCondition.register() ZoneCondition.register()
NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners) NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners)
Guardians.register() Guardians.register()
Gates.register() Gates.register()
OreNodes.register()
Catching.register()
ZoneHazards.register() ZoneHazards.register()
LevelPressure.register() LevelPressure.register()
SelfTest.register() SelfTest.register()
@@ -0,0 +1,113 @@
package dev.cobblesurvival.catching
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokeball.PokeBallCaptureCalculatedEvent
import com.cobblemon.mod.common.api.events.pokeball.PokemonCatchRateEvent
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.google.gson.Gson
import com.google.gson.JsonElement
import dev.cobblesurvival.CobbleSurvival
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.resources.ResourceManager
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
import net.minecraft.util.profiling.ProfilerFiller
import net.neoforged.neoforge.common.NeoForge
import net.neoforged.neoforge.event.AddReloadListenerEvent
import net.neoforged.neoforge.event.tick.ServerTickEvent
/**
* Catching (Adrian, 2026-10-30): high-level Pokémon are hard to catch. Everything is measured against the trainer's
* strongest party Pokémon:
* - the catch rate falls with the level gap, down to a small share at 20 levels above ("very improbable");
* - a Pokémon that breaks out of a ball attacks the thrower instead of fleeing;
* - a wild Pokémon above a nearby player's strongest Pokémon is hostile to that player.
*
* The attacking is Fight or Flight Reborn's: an angry Pokémon carries the aspect [ENRAGED], which the pack's Fight or
* Flight config lists in `always_aggro_aspects` (so it fights rather than flees), and its target is set through Fight
* or Flight's own "caught by" mark ([FightOrFlight]). While it has a target, Fight or Flight doesn't let thrown balls
* catch it (`aggressive_pokemon_catchable` false): it has to be battled. Values: `data/cobblesurvival/catching/`.
*/
object Catching : SimpleJsonResourceReloadListener(Gson(), "catching") {
const val ENRAGED = "cobblesurvival_enraged"
private const val SCAN_TICKS = 20
class Rules(
val fullUpTo: Int = 0,
val minimumAt: Int = 20,
val minimum: Double = 0.02,
val enrageOnFailedCatch: Boolean = true,
val hostileWhenStronger: Boolean = true,
val hostileRange: Double = 16.0,
)
var rules = Rules()
private set
/** The self-tests switch the hostility scan off, except in their own batch: other tests put weak trainers near wild Pokémon. */
var hostilityPaused = false
fun register() {
NeoForge.EVENT_BUS.addListener<AddReloadListenerEvent> { it.addListener(this) }
CobblemonEvents.POKEMON_CATCH_RATE.subscribe(Priority.LOWEST, ::onCatchRate)
CobblemonEvents.POKE_BALL_CAPTURE_CALCULATED.subscribe(Priority.NORMAL, ::onCaptureCalculated)
NeoForge.EVENT_BUS.addListener(::onServerTick)
}
override fun apply(files: Map<ResourceLocation, JsonElement>, resources: ResourceManager, profiler: ProfilerFiller) {
val json = files[ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "rules")]?.asJsonObject ?: return
val rate = json.getAsJsonObject("catch_rate")
rules = Rules(
fullUpTo = rate.get("full_up_to_levels_above").asInt,
minimumAt = rate.get("minimum_at_levels_above").asInt,
minimum = rate.get("minimum").asDouble,
enrageOnFailedCatch = json.get("failed_catch_enrages").asBoolean,
hostileWhenStronger = json.get("hostile_when_stronger").asBoolean,
hostileRange = json.get("hostile_range").asDouble,
)
}
fun strongest(player: ServerPlayer): Int = Cobblemon.storage.getParty(player).maxOfOrNull { it.level } ?: 0
/** The share of its catch rate a Pokémon keeps [gap] levels above the thrower's strongest Pokémon. */
fun catchFactor(gap: Int, r: Rules = rules): Double = when {
gap <= r.fullUpTo -> 1.0
gap >= r.minimumAt -> r.minimum
else -> 1.0 - (gap - r.fullUpTo).toDouble() / (r.minimumAt - r.fullUpTo) * (1.0 - r.minimum)
}
private fun onCatchRate(event: PokemonCatchRateEvent) {
val player = event.thrower as? ServerPlayer ?: return
val factor = catchFactor(event.pokemonEntity.pokemon.level - strongest(player))
if (factor < 1.0) event.catchRate = (event.catchRate * factor).toFloat()
}
private fun onCaptureCalculated(event: PokeBallCaptureCalculatedEvent) {
if (!rules.enrageOnFailedCatch || event.captureResult.isSuccessfulCapture) return
enrage(event.pokemonEntity, event.thrower as? ServerPlayer ?: return)
}
private fun onServerTick(event: ServerTickEvent.Post) {
if (!rules.hostileWhenStronger || hostilityPaused || event.server.tickCount % SCAN_TICKS != 0) return
for (player in event.server.playerList.players) {
if (player.isCreative || player.isSpectator) continue
val strongest = strongest(player)
val near = player.boundingBox.inflate(rules.hostileRange)
player.serverLevel().getEntitiesOfClass(PokemonEntity::class.java, near) {
it.pokemon.isWild() && !it.isBattling && it.pokemon.level > strongest
}.forEach { enrage(it, player) }
}
}
/** Makes [wild] fight [player] (Fight or Flight does the fighting). */
fun enrage(wild: PokemonEntity, player: ServerPlayer) {
val pokemon = wild.pokemon
if (ENRAGED !in pokemon.forcedAspects) {
pokemon.forcedAspects = pokemon.forcedAspects + ENRAGED
pokemon.updateAspects()
}
FightOrFlight.setTarget(wild, player)
}
}
@@ -0,0 +1,33 @@
package dev.cobblesurvival.catching
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import net.minecraft.server.level.ServerPlayer
import net.neoforged.fml.ModList
/**
* Fight or Flight Reborn, by reflection (not a compile dependency). [setTarget] uses its "caught by" mark, the same
* one a failed catch sets: its sensor then makes the player the Pokémon's attack target. [aggression] is its
* fight-or-flight value (above its neutral threshold the Pokémon fights, below it flees).
*/
object FightOrFlight {
val installed: Boolean get() = ModList.get().isLoaded("fightorflight")
private val setCapturedBy by lazy {
Class.forName("me.rufia.fightorflight.PokemonInterface").getMethod("setCapturedBy", Int::class.javaPrimitiveType)
}
private val coefficient by lazy {
Class.forName("me.rufia.fightorflight.CobblemonFightOrFlight").getMethod("getFightOrFlightCoefficient", PokemonEntity::class.java)
}
fun setTarget(wild: PokemonEntity, player: ServerPlayer) {
if (installed) setCapturedBy.invoke(wild, player.id)
}
fun aggression(wild: PokemonEntity): Double = coefficient.invoke(null, wild) as Double
/** Fight or Flight's `neutral_threshold`: at or above it a provoked Pokémon fights. */
fun neutralThreshold(): Double {
val config = Class.forName("me.rufia.fightorflight.CobblemonFightOrFlight").getMethod("commonConfig").invoke(null)
return (config.javaClass.getField("neutral_threshold").get(config) as Number).toDouble()
}
}
@@ -1,6 +1,7 @@
package dev.cobblesurvival.dev package dev.cobblesurvival.dev
import com.mojang.brigadier.builder.LiteralArgumentBuilder import com.mojang.brigadier.builder.LiteralArgumentBuilder
import dev.cobblesurvival.ore.OreNodes
import net.minecraft.commands.CommandSourceStack import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands import net.minecraft.commands.Commands
import net.minecraft.commands.arguments.coordinates.ColumnPosArgument import net.minecraft.commands.arguments.coordinates.ColumnPosArgument
@@ -8,7 +9,10 @@ import net.minecraft.core.BlockPos
import net.minecraft.network.chat.Component import net.minecraft.network.chat.Component
import net.minecraft.server.level.ColumnPos import net.minecraft.server.level.ColumnPos
import net.minecraft.server.level.ServerLevel import net.minecraft.server.level.ServerLevel
import net.minecraft.util.RandomSource
import net.minecraft.world.level.ChunkPos import net.minecraft.world.level.ChunkPos
import net.neoforged.fml.ModList
import java.util.Optional
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -17,20 +21,47 @@ import kotlin.math.min
* dependency of the companion mod). * dependency of the companion mod).
* *
* Dev tool: `/cobblesurvival veins unfilled <x1> <z1> <x2> <z2>` counts the loaded chunks in a block rectangle that * Dev tool: `/cobblesurvival veins unfilled <x1> <z1> <x2> <z2>` counts the loaded chunks in a block rectangle that
* the mod hasn't put veins into yet. It fills a new chunk only while that chunk is loaded, 8 chunks a tick, and * the mod hasn't put veins into yet, or the companion mod its ore nodes ([OreNodes]). Both fill a new chunk only
* forgets the rest when the server stops, so a pregeneration run outpaces it; tools/make_world.py loads the island * while that chunk is loaded, a few chunks a tick, and the vein mod forgets the rest when the server stops, so a
* again row by row and waits until this count is 0 around each row (loading a row also fully loads the chunks next * pregeneration run outpaces them; tools/make_world.py loads the island again row by row and waits until this count
* to it). The self-tests use [markMined] to wind a vein block's clock back. * is 0 around each row (loading a row also fully loads the chunks next to it). [place] puts a vein down the way
* `/regenerating_ore_veins place` does (the ore nodes); the self-tests use [markMined] to wind a vein block's clock
* back and [veinAt] to see what was placed.
*/ */
object OreVeins { object OreVeins {
private val savedData by lazy { Class.forName("net.stargazer.regenerating_ore_veins.VeinSavedData") } private const val ROV = "net.stargazer.regenerating_ore_veins"
val installed: Boolean get() = ModList.get().isLoaded("regenerating_ore_veins")
private val savedData by lazy { Class.forName("$ROV.VeinSavedData") }
private val get by lazy { savedData.getMethod("get", ServerLevel::class.java) } private val get by lazy { savedData.getMethod("get", ServerLevel::class.java) }
private val isChunkGenerated by lazy { savedData.getMethod("isChunkGenerated", Long::class.javaPrimitiveType) } private val isChunkGenerated by lazy { savedData.getMethod("isChunkGenerated", Long::class.javaPrimitiveType) }
private val markMined by lazy { savedData.getMethod("markMined", BlockPos::class.java, Long::class.javaPrimitiveType) } private val markMined by lazy { savedData.getMethod("markMined", BlockPos::class.java, Long::class.javaPrimitiveType) }
private val getEntry by lazy { savedData.getMethod("getEntry", BlockPos::class.java) }
private val getVein by lazy { Class.forName("$ROV.VeinConfig").getMethod("getVein", String::class.java) }
private val placeManagedVein by lazy {
Class.forName("$ROV.VeinRuntime").getMethod("placeManagedVein", ServerLevel::class.java, BlockPos::class.java,
Class.forName("$ROV.VeinConfig\$VeinDefinition"), RandomSource::class.java, Boolean::class.javaPrimitiveType)
}
fun isChunkFilled(level: ServerLevel, chunk: ChunkPos): Boolean = fun isChunkFilled(level: ServerLevel, chunk: ChunkPos): Boolean =
isChunkGenerated.invoke(get.invoke(null, level), chunk.toLong()) as Boolean isChunkGenerated.invoke(get.invoke(null, level), chunk.toLong()) as Boolean
/**
* Places vein [id] around [center] whatever is there (forced, like the mod's place command), as a tracked vein
* that grows back; returns how many blocks it placed (0 if there's no such vein).
*/
fun place(level: ServerLevel, center: BlockPos, id: String, random: RandomSource): Int {
val definition = (getVein.invoke(null, id) as Optional<*>).orElse(null) ?: return 0
return placeManagedVein.invoke(null, level, center, definition, random, true) as Int
}
/** The id of the vein that tracks the block at [pos], or null. */
fun veinAt(level: ServerLevel, pos: BlockPos): String? {
val entry = getEntry.invoke(get.invoke(null, level), pos) ?: return null
return entry.javaClass.getMethod("veinId").invoke(entry) as String
}
/** Sets when the vein block at [pos] was last mined, in seconds since the epoch (the mod counts real time). */ /** Sets when the vein block at [pos] was last mined, in seconds since the epoch (the mod counts real time). */
fun markMined(level: ServerLevel, pos: BlockPos, epochSecond: Long) { fun markMined(level: ServerLevel, pos: BlockPos, epochSecond: Long) {
markMined.invoke(get.invoke(null, level), pos, epochSecond) markMined.invoke(get.invoke(null, level), pos, epochSecond)
@@ -46,7 +77,7 @@ object OreVeins {
val level = context.source.server.overworld() val level = context.source.server.overworld()
val unfilled = chunks(ColumnPosArgument.getColumnPos(context, "from"), val unfilled = chunks(ColumnPosArgument.getColumnPos(context, "from"),
ColumnPosArgument.getColumnPos(context, "to")) ColumnPosArgument.getColumnPos(context, "to"))
.count { level.chunkSource.hasChunk(it.x, it.z) && !isChunkFilled(level, it) } .count { level.chunkSource.hasChunk(it.x, it.z) && !(isChunkFilled(level, it) && OreNodes.isDone(level, it)) }
context.source.sendSuccess({ Component.literal("unfilled chunks: $unfilled") }, false) context.source.sendSuccess({ Component.literal("unfilled chunks: $unfilled") }, false)
1 1
}))) })))
@@ -0,0 +1,201 @@
package dev.cobblesurvival.ore
import com.google.gson.Gson
import com.google.gson.JsonElement
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.dev.OreVeins
import dev.cobblesurvival.zone.Zones
import it.unimi.dsi.fastutil.longs.LongOpenHashSet
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.core.registries.Registries
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.LongArrayTag
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.packs.resources.ResourceManager
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
import net.minecraft.tags.TagKey
import net.minecraft.util.RandomSource
import net.minecraft.util.profiling.ProfilerFiller
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.biome.Biome
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.level.saveddata.SavedData
import net.neoforged.neoforge.common.NeoForge
import net.neoforged.neoforge.event.AddReloadListenerEvent
import net.neoforged.neoforge.event.level.ChunkEvent
import net.neoforged.neoforge.event.server.ServerStoppedEvent
import net.neoforged.neoforge.event.tick.LevelTickEvent
import kotlin.math.max
/**
* Ore nodes (Adrian, 2026-10-30): ore shows up "mainly on the surface" as small nodes, while the larger veins
* underground are harder to find, and the cave zones' ores sit as nodes in the cave, "not in the walls". In every new
* overworld chunk the companion mod places its zone's nodes: a lump of ore in the natural ground (never in water or
* under leaves), or on a cave floor in the tier's cave biome. They are Regenerating Ore Veins veins placed through
* the mod ([OreVeins.place]), so they grow back like any vein. The underground veins are the vein mod's own.
*
* The node table comes from tools/zone_ores.py: `data/cobblesurvival/ore_nodes/<name>.json` (which node vein, zone,
* chance per chunk, size; cave nodes also the biome, the depth and how many columns to search). Ground is the block
* tag `cobblesurvival:ore_node_ground`. Like the vein mod, nodes go into a new chunk once it is loaded, a few chunks a
* tick; the chunks done are kept in the world save ([NodeState]), and `/cobblesurvival veins unfilled` counts both.
*/
object OreNodes : SimpleJsonResourceReloadListener(Gson(), "ore_nodes") {
class Node(
val vein: String,
val zone: String,
val cave: Boolean,
val chance: Double,
val radius: Int,
val biome: ResourceKey<Biome>?,
val tries: Int,
val minY: Int,
val maxY: Int,
)
private const val CHUNKS_PER_TICK = 8
private const val SURFACE_TRIES = 4
/** A self-test placing a node on purpose searches this many times longer. */
private const val FORCED_TRIES = 8
val GROUND: TagKey<Block> = TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "ore_node_ground"))
var nodes: List<Node> = emptyList()
private set
private val pending = LongOpenHashSet()
fun register() {
NeoForge.EVENT_BUS.addListener<AddReloadListenerEvent> { it.addListener(this) }
NeoForge.EVENT_BUS.addListener(::onChunkLoad)
NeoForge.EVENT_BUS.addListener(::onLevelTick)
NeoForge.EVENT_BUS.addListener<ServerStoppedEvent> { pending.clear() }
}
override fun apply(files: Map<ResourceLocation, JsonElement>, resources: ResourceManager, profiler: ProfilerFiller) {
nodes = files.toSortedMap().values.flatMap { file ->
file.asJsonObject.getAsJsonArray("nodes").map { element ->
val json = element.asJsonObject
val y = json.getAsJsonArray("y")
Node(
vein = json.get("vein").asString,
zone = json.get("zone").asString,
cave = json.get("where").asString == "cave",
chance = json.get("chance").asDouble,
radius = json.get("radius").asInt,
biome = json.get("biome")?.asString?.let { ResourceKey.create(Registries.BIOME, ResourceLocation.parse(it)) },
tries = json.get("tries")?.asInt ?: SURFACE_TRIES,
minY = y?.get(0)?.asInt ?: 0,
maxY = y?.get(1)?.asInt ?: 0,
)
}
}
CobbleSurvival.LOGGER.info("Loaded {} ore node kinds", nodes.size)
}
fun isDone(level: ServerLevel, chunk: ChunkPos) = NodeState.of(level).isDone(chunk)
private fun onChunkLoad(event: ChunkEvent.Load) {
val level = event.level as? ServerLevel ?: return
if (event.isNewChunk && level.dimension() == Level.OVERWORLD) pending.add(event.chunk.pos.toLong())
}
private fun onLevelTick(event: LevelTickEvent.Post) {
val level = event.level as? ServerLevel ?: return
if (level.dimension() != Level.OVERWORLD || pending.isEmpty() || !OreVeins.installed) return
val state = NodeState.of(level)
var filled = 0
val chunks = pending.iterator()
while (chunks.hasNext() && filled < CHUNKS_PER_TICK) {
val chunk = ChunkPos(chunks.nextLong())
if (state.isDone(chunk)) {
chunks.remove()
} else if (level.chunkSource.hasChunk(chunk.x, chunk.z)) {
fill(level, chunk)
state.markDone(chunk)
chunks.remove()
filled++
}
}
}
/**
* Places the nodes of [chunk] (only [only], if given) and returns where each one sits: the ground or cave-floor
* block it was put into. [force] ignores the chance and searches longer (self-tests).
*/
fun fill(level: ServerLevel, chunk: ChunkPos, only: Node? = null, force: Boolean = false): List<BlockPos> {
val placed = ArrayList<BlockPos>()
for (node in only?.let(::listOf) ?: nodes) {
val random = RandomSource.create(level.seed xor (chunk.toLong() * 0x9E3779B97F4A7C15uL.toLong()) xor node.vein.hashCode().toLong())
if (!force && random.nextDouble() >= node.chance) continue
val tries = node.tries * (if (force) FORCED_TRIES else 1)
val ground = (if (node.cave) caveFloor(level, chunk, node, random, tries) else surface(level, chunk, node, random, tries)) ?: continue
// Sunk so that at most one block stands out: radius r sits r-1 below the ground block.
if (OreVeins.place(level, ground.below(max(0, node.radius - 1)), node.vein, random) > 0) placed.add(ground)
}
return placed
}
/** A random column, kept [Node.radius] blocks inside the chunk so a node never reaches into the next one. */
private fun column(chunk: ChunkPos, node: Node, random: RandomSource): Pair<Int, Int> {
val span = 16 - 2 * node.radius
return chunk.minBlockX + node.radius + random.nextInt(span) to chunk.minBlockZ + node.radius + random.nextInt(span)
}
private fun surface(level: ServerLevel, chunk: ChunkPos, node: Node, random: RandomSource, tries: Int): BlockPos? {
repeat(tries) {
val (x, z) = column(chunk, node, random)
val ground = BlockPos(x, level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1, z)
val state = level.getBlockState(ground)
val above = level.getBlockState(ground.above())
if (state.`is`(GROUND) && state.fluidState.isEmpty && (above.isAir || above.canBeReplaced()) && above.fluidState.isEmpty &&
inZone(level, ground, node)) return ground
}
return null
}
/** The whole footprint in the node's zone: zone cells are squares, so checking the corners is enough. */
private fun inZone(level: ServerLevel, ground: BlockPos, node: Node): Boolean =
listOf(-node.radius, node.radius).all { dx -> listOf(-node.radius, node.radius).all { dz -> Zones.at(level, ground.offset(dx, 0, dz))?.id == node.zone } }
private fun caveFloor(level: ServerLevel, chunk: ChunkPos, node: Node, random: RandomSource, tries: Int): BlockPos? {
val biome = node.biome ?: return null
repeat(tries) {
val (x, z) = column(chunk, node, random)
for (y in node.maxY downTo node.minY + 1) {
val air = BlockPos(x, y, z)
if (!level.getBlockState(air).isAir || !level.getBlockState(air.below()).`is`(GROUND)) continue
if (level.getBiome(air).`is`(biome)) return air.below()
}
}
return null
}
}
/** The overworld chunks [OreNodes] has filled. Saved with the overworld. */
class NodeState : SavedData() {
private val done = LongOpenHashSet()
fun isDone(chunk: ChunkPos) = done.contains(chunk.toLong())
fun markDone(chunk: ChunkPos) {
if (done.add(chunk.toLong())) setDirty()
}
override fun save(tag: CompoundTag, registries: HolderLookup.Provider): CompoundTag {
tag.put("done", LongArrayTag(done.toLongArray()))
return tag
}
companion object {
private val FACTORY = Factory(::NodeState, ::load)
fun of(level: ServerLevel): NodeState = level.dataStorage.computeIfAbsent(FACTORY, "cobblesurvival_ore_nodes")
private fun load(tag: CompoundTag, registries: HolderLookup.Provider): NodeState =
NodeState().apply { done.addAll(LongOpenHashSet(tag.getLongArray("done"))) }
}
}
@@ -0,0 +1,90 @@
package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokeball.PokeBallCaptureCalculatedEvent
import com.cobblemon.mod.common.api.events.pokeball.PokemonCatchRateEvent
import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import dev.cobblesurvival.catching.Catching
import dev.cobblesurvival.catching.FightOrFlight
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import kotlin.math.abs
/**
* Catching (Adrian, 2026-10-30), against the trainer's strongest party Pokémon: the catch rate falls with the level
* gap (20+ levels above: very improbable), a failed catch makes the Pokémon attack the thrower, and a wild Pokémon
* above a nearby trainer's strongest is hostile to them. The hostility cases run in batches of their own, with the
* scan switched on only there (other tests put weak trainers next to wild Pokémon).
*/
object CatchingTests {
fun all() = listOf(
SelfTestCase("catch_rate_falls_with_level_gap", 20, ::catchRate, batch = "catching"),
SelfTestCase("failed_catch_enrages", 400, ::failedCatch, batch = "catching"),
SelfTestCase("strong_wild_pokemon_hunt_weak_trainers", 400, { s -> hostility(s, strongest = 5, hostile = true) },
batch = "catching_weak"),
SelfTestCase("wild_pokemon_ignore_stronger_trainers", 120, { s -> hostility(s, strongest = 50, hostile = false) },
batch = "catching_strong"),
)
private const val WILD_LEVEL = 30
private fun catchRate(s: Scenario) {
val player = s.player()
s.give(player, s.pokemon("pidgey", 10))
s.check(Catching.catchFactor(20) <= 0.05) { "20 levels above still keeps ${Catching.catchFactor(20)} of the catch rate" }
for (level in listOf(10, 15, 20, 30, 45)) {
val wild = PokemonEntity(s.level, s.pokemon("rattata", level))
val event = PokemonCatchRateEvent(player, EmptyPokeBallEntity(s.level), wild, 100f)
CobblemonEvents.POKEMON_CATCH_RATE.post(event)
val expected = 100.0 * Catching.catchFactor(level - 10)
s.check(abs(event.catchRate - expected) < 0.01) { "Lv $level against Lv 10: catch rate ${event.catchRate}, expected $expected" }
}
s.helper.succeed()
}
private fun failedCatch(s: Scenario) {
val player = s.player()
s.give(player, s.pokemon("pidgeot", 60)) // stronger than the wild one: only the failed catch angers it
val wild = s.wild(s.pokemon("bidoof", 20), s.at(7.5, 1.0, 7.5))
s.helper.startSequence()
.thenWaitUntil { s.check(wild.pokemon.entity === wild) { "the wild Pokémon hasn't ticked yet" } }
.thenExecute {
CobblemonEvents.POKE_BALL_CAPTURE_CALCULATED.post(
PokeBallCaptureCalculatedEvent(player, wild, EmptyPokeBallEntity(s.level), CaptureContext(1, false, false))
)
s.check(Catching.ENRAGED in wild.pokemon.aspects) { "a failed catch didn't anger the Pokémon" }
val aggression = FightOrFlight.aggression(wild)
s.check(aggression >= FightOrFlight.neutralThreshold()) { "the angry Pokémon would flee (aggression $aggression)" }
FightOrFlight.setTarget(wild, player) // what Fight or Flight does when the ball breaks open
}
.thenWaitUntil { checkTarget(s, wild, player) }
.thenSucceed()
}
private fun hostility(s: Scenario, strongest: Int, hostile: Boolean) {
Catching.hostilityPaused = false
s.afterTest { Catching.hostilityPaused = true }
val player = s.player()
s.give(player, s.pokemon("pidgey", strongest))
val wild = s.wild(s.pokemon("machop", WILD_LEVEL), s.at(9.5, 1.0, 9.5))
val sequence = s.helper.startSequence()
.thenWaitUntil { s.check(wild.pokemon.entity === wild) { "the wild Pokémon hasn't ticked yet" } }
if (hostile) {
sequence.thenWaitUntil {
s.check(Catching.ENRAGED in wild.pokemon.aspects) { "a Lv $WILD_LEVEL wild Pokémon isn't angry at a trainer whose best is Lv $strongest" }
checkTarget(s, wild, player)
}.thenSucceed()
} else {
sequence.thenIdle(80).thenExecute {
s.check(Catching.ENRAGED !in wild.pokemon.aspects) { "a Lv $WILD_LEVEL wild Pokémon got angry at a trainer with a Lv $strongest" }
}.thenSucceed()
}
}
private fun checkTarget(s: Scenario, wild: PokemonEntity, player: ServerPlayer) {
val target = wild.brain.getMemory(MemoryModuleType.ATTACK_TARGET).orElse(null)
s.check(target === player) { "the angry Pokémon targets ${target?.name?.string ?: "nobody"}, not the player" }
}
}
@@ -0,0 +1,54 @@
package dev.cobblesurvival.selftest
import dev.cobblesurvival.dev.OreVeins
import dev.cobblesurvival.ore.OreNodes
import dev.cobblesurvival.zone.Zones
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.world.level.ChunkPos
/**
* Ore nodes on the island (Adrian, 2026-10-30): a surface node is a small lump of ore in the ground of its zone with
* open air above it; a cave node lies on the floor of its cave biome, touching the cave's air, not buried in a wall.
* Each test places one node on purpose near its zone's sample point and checks it as Regenerating Ore Veins tracks it.
*/
object OreNodeTests {
fun all() = listOf(
SelfTestCase("surface_nodes_lie_in_the_ground", 20, { s -> nodeTest(s, "button_isle", cave = false) }, islandOnly = true),
SelfTestCase("cave_nodes_lie_on_the_cave_floor", 20, { s -> nodeTest(s, "deep_dark", cave = true) }, islandOnly = true),
)
private fun nodeTest(s: Scenario, zone: String, cave: Boolean) {
val level = s.level
val node = OreNodes.nodes.firstOrNull { it.zone == zone && it.cave == cave }
?: throw GameTestAssertException("no ${if (cave) "cave" else "surface"} node kind in $zone")
val sample = Zones.samples[zone] ?: throw GameTestAssertException("no sample point for $zone")
val home = ChunkPos(BlockPos(sample.x, 0, sample.z))
// Next to the sample chunk first (the guardian stands on the sample point), then around it.
val chunks = listOf(ChunkPos(home.x + 2, home.z)) + (-1..1).flatMap { dx -> (-1..1).map { dz -> ChunkPos(home.x + dx, home.z + dz) } }
val grounds = chunks.asSequence()
.onEach { level.getChunk(it.x, it.z) }
.map { OreNodes.fill(level, it, only = node, force = true) }
.firstOrNull { it.isNotEmpty() }
?: throw GameTestAssertException("no spot for a ${node.vein} near $zone's sample point")
for (ground in grounds) {
val blocks = BlockPos.betweenClosed(ground.offset(-node.radius - 1, -node.radius - 1, -node.radius - 1),
ground.offset(node.radius + 1, node.radius + 1, node.radius + 1))
.map { it.immutable() }
.filter { OreVeins.veinAt(level, it) == node.vein }
s.check(blocks.isNotEmpty()) { "the ${node.vein} at ${ground.toShortString()} isn't tracked by Regenerating Ore Veins" }
val exposed = blocks.any { block -> Direction.entries.any { level.getBlockState(block.relative(it)).isAir } }
s.check(exposed) { "the ${node.vein} at ${ground.toShortString()} touches no air: it's buried" }
if (cave) {
s.check(level.getBiome(ground.above()).`is`(node.biome!!)) { "the ${node.vein} at ${ground.toShortString()} isn't in ${node.biome}" }
} else {
val top = blocks.maxBy { it.y }
s.check(level.canSeeSky(top.above())) { "the ${node.vein} at ${ground.toShortString()} is under cover" }
s.check(blocks.all { Zones.at(level, it)?.id == zone }) { "the ${node.vein} at ${ground.toShortString()} reaches out of $zone" }
}
}
s.helper.succeed()
}
}
@@ -2,6 +2,7 @@ package dev.cobblesurvival.selftest
import com.mojang.brigadier.builder.LiteralArgumentBuilder import com.mojang.brigadier.builder.LiteralArgumentBuilder
import dev.cobblesurvival.CobbleSurvival import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.catching.Catching
import net.minecraft.commands.CommandSourceStack import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands import net.minecraft.commands.Commands
import net.minecraft.core.BlockPos import net.minecraft.core.BlockPos
@@ -51,7 +52,8 @@ object SelfTest {
private val cases: List<SelfTestCase> get() = private val cases: List<SelfTestCase> get() =
PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all() + GuardianTests.all() + PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all() + GuardianTests.all() +
VeinTests.all() + DropTests.all() + ProgressionTests.all() VeinTests.all() + OreNodeTests.all() + DropTests.all() + ProgressionTests.all() +
CatchingTests.all()
private var running: List<Scenario> = emptyList() private var running: List<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList() private var forced: List<ChunkPos> = emptyList()
@@ -99,6 +101,7 @@ object SelfTest {
GameTestInfo(function, Rotation.NONE, level, RetryOptions.noRetries()).also { it.addListener(scenario) } GameTestInfo(function, Rotation.NONE, level, RetryOptions.noRetries()).also { it.addListener(scenario) }
} }
running = scenarios running = scenarios
Catching.hostilityPaused = true
GameTestTicker.SINGLETON.clear() GameTestTicker.SINGLETON.clear()
GameTestRunner.Builder.fromInfo(infos, level) GameTestRunner.Builder.fromInfo(infos, level)
.newStructureSpawner(StructureGridSpawner(origin, TESTS_PER_ROW, false)) .newStructureSpawner(StructureGridSpawner(origin, TESTS_PER_ROW, false))
@@ -128,5 +131,6 @@ object SelfTest {
val level = event.server.overworld() val level = event.server.overworld()
forced.forEach { level.setChunkForced(it.x, it.z, false) } forced.forEach { level.setChunkForced(it.x, it.z, false) }
running = emptyList() running = emptyList()
Catching.hostilityPaused = false
} }
} }
@@ -9,20 +9,24 @@ import net.minecraft.server.level.ServerLevel
import net.minecraft.tags.TagKey import net.minecraft.tags.TagKey
import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.EntityType
import net.minecraft.world.entity.Mob import net.minecraft.world.entity.Mob
import net.minecraft.world.entity.animal.Bee
import net.minecraft.world.level.block.Block import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.InfestedBlock import net.minecraft.world.level.block.InfestedBlock
import net.minecraft.world.level.block.SculkShriekerBlock import net.minecraft.world.level.block.SculkShriekerBlock
import net.minecraft.world.level.block.entity.BeehiveBlockEntity
import net.minecraft.world.level.chunk.LevelChunk import net.minecraft.world.level.chunk.LevelChunk
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent
import net.neoforged.neoforge.event.level.ChunkEvent import net.neoforged.neoforge.event.level.ChunkEvent
import net.neoforged.neoforge.event.tick.ServerTickEvent
/** /**
* Only Pokémon live on the island. Structures stay, but their vanilla mobs and mob sources go: * Only Pokémon live on the island. Structures stay, but their vanilla mobs and mob sources go:
* - a mob that isn't Cobblemon's (Pokémon, trainers) never joins a world unless it is loaded from a save. * - a mob that isn't Cobblemon's (Pokémon, trainers) never joins a world unless it is loaded from a save.
* That covers mobs placed by structures during world generation (villagers, guardians, shulkers, ...), * That covers mobs placed by structures during world generation (villagers, guardians, shulkers, ...),
* other mods' animals (Cold Sweat's chameleons) and anything that would spawn later. Exceptions: the * other mods' animals (Cold Sweat's chameleons) and anything that would spawn later. Exceptions: the
* entity type tag `cobblesurvival:allowed_vanilla_mobs`. * entity type tag `cobblesurvival:allowed_vanilla_mobs`. A bee nest whose bee is turned away is emptied:
* otherwise it keeps the bee, tries to let it out again every tick and plays its sound each time.
* - when a chunk has just been generated, blocks in the tag `cobblesurvival:removed_on_generation` * - when a chunk has just been generated, blocks in the tag `cobblesurvival:removed_on_generation`
* (spawners, trial spawners) become air, sculk shriekers can no longer summon the Warden, and * (spawners, trial spawners) become air, sculk shriekers can no longer summon the Warden, and
* infested blocks (silverfish) become their normal block. Chunks loaded from disk are never touched. * infested blocks (silverfish) become their normal block. Chunks loaded from disk are never touched.
@@ -34,12 +38,36 @@ object OnlyPokemon {
TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "removed_on_generation")) TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "removed_on_generation"))
private const val FLAGS = Block.UPDATE_CLIENTS or Block.UPDATE_KNOWN_SHAPE private const val FLAGS = Block.UPDATE_CLIENTS or Block.UPDATE_KNOWN_SHAPE
/** Nests whose bees were just turned away; emptied at the end of the tick, since the nest is mid-loop over them. */
private val hivesToEmpty = HashSet<Pair<ServerLevel, BlockPos>>()
fun onEntityJoin(event: EntityJoinLevelEvent) { fun onEntityJoin(event: EntityJoinLevelEvent) {
val mob = event.entity as? Mob ?: return val mob = event.entity as? Mob ?: return
if (event.level !is ServerLevel || event.loadedFromDisk()) return val level = event.level as? ServerLevel ?: return
if (event.loadedFromDisk()) return
if (BuiltInRegistries.ENTITY_TYPE.getKey(mob.type).namespace == "cobblemon") return if (BuiltInRegistries.ENTITY_TYPE.getKey(mob.type).namespace == "cobblemon") return
if (mob.type.`is`(ALLOWED_MOBS)) return if (mob.type.`is`(ALLOWED_MOBS)) return
event.isCanceled = true event.isCanceled = true
if (mob is Bee) {
// A nest keeps a bee it can't release and tries again every tick, with its sound each time; empty it.
val at = mob.blockPosition()
BlockPos.betweenClosed(at.offset(-2, -2, -2), at.offset(2, 2, 2))
.filter { level.getBlockEntity(it) is BeehiveBlockEntity }
.forEach { hivesToEmpty.add(level to it.immutable()) }
}
}
fun onServerTick(event: ServerTickEvent.Post) {
if (hivesToEmpty.isEmpty()) return
for ((level, pos) in hivesToEmpty) {
val hive = level.getBlockEntity(pos) as? BeehiveBlockEntity ?: continue
// Loading a nest's data clears its bees and reads them back from "bees"; without that list it stays empty.
val data = hive.saveWithoutMetadata(level.registryAccess())
data.remove("bees")
hive.loadWithComponents(data, level.registryAccess())
hive.setChanged()
}
hivesToEmpty.clear()
} }
fun onChunkLoad(event: ChunkEvent.Load) { fun onChunkLoad(event: ChunkEvent.Load) {
+382
View File
@@ -0,0 +1,382 @@
{
// Do more aggressive Pokemon fight back when provoked?
"do_pokemon_attack": true,
// Do especially aggressive Pokemon attack unprovoked? Required for the other configs that make Pokemon always aggressive
"do_pokemon_attack_unprovoked": false,
// If the aggressive pokemon will only attack unprovoked in the dark area.(similar to the spider in Minecraft,do_pokemon_attack_unprovoked needs to be set to true
"light_dependent_unprovoked_attack": true,
// If failed captures can be counted as provocation?
"failed_capture_counted_as_provocation": true,
// Do aggro Pokemon attack their targets even if they're in the middle of a battles?
"do_pokemon_attack_in_battle": false,
// If the aggressive pokemon can be caught by Poke Ball.
"aggressive_pokemon_catchable": false,
// The minimum level a Pokemon needs to be to fight back when provoked.
"minimum_attack_level": 5,
// The minimum level a Pokemon needs to be to attack unprovoked.
"minimum_attack_unprovoked_level": 10,
// If this is set to true, player's Pokemon won't attack shiny Pokemon
"not_attacking_wild_shiny": true,
// If a wild Pokemon's aggro is above this value, it will attack the player proactively
"aggressive_threshold": 100.0,
// If a wild Pokemon's aggro is above this value and lower than the aggressive_threshold, it will fight back when attacked,if the Pokemon's aggro is lower than this, it will just run away.
"neutral_threshold": 20.0,
// The value of the aggression if the Pokemon's level reaches 100.
"aggression_level_base_value": 40.0,
// The multiplier used to calculate the wild Pokemon's aggression,lower value can make the high level Pokemon less aggressive. Editing the base value is enough, This is kept for the old config files. This still works, though
"aggression_level_multiplier": 1.0,
// The value of the aggression if [(Atk + Sp. Atk)-(Def + Sp. Def)]/level = 1. The Pokemon level won't be affected by the multiplier above.
"aggression_atk_def_dif_base_value": 30.0,
// Are Dark types more aggressive at or below light level 7 and less aggressive at or above light level 12?
"dark_light_level_aggro": true,
// Are Ghost types more aggressive at or below light level 7 and less aggressive at or above light level 12?
"ghost_light_level_aggro": true,
// The value of the aggression if the conditions above are met
"aggression_light_level_base_value": 30.0,
// The value of the aggression influenced by the Pokemon's nature.
"aggression_nature_base_value": 40.0,
// The value of the aggression gained if a Pokemon is intimidated
"aggression_intimidation_base_value": -30.0,
// Pokemon below this map height (y) will always be aggressive.
"always_aggro_below": -128.0,
// Pokemon stops running away if the hp is not full.
"stop_running_after_hurt": true,
// Wild pokemon will be slow down if the hp is not full.
"slow_down_after_hurt": false,
// Make all the Alpha Pokemons aggressive. It has higher priority than never aggro species currently.
"alpha_always_aggressive": true,
// Pokemon with these natures are slightly more aggressive.
"aggressive_nature": [
"sassy",
"hardy",
"bold",
"impish",
"hasty"
],
// The aggression multiplier for natures above.
"aggressive_nature_multiplier": 1.0,
// Pokemon with these natures are much more aggressive.
"more_aggressive_nature": [
"brave",
"rash",
"adamant",
"naughty"
],
// The aggression multiplier for natures above.
"more_aggressive_nature_multiplier": 2.0,
// Pokemon with these natures are slightly more peaceful.
"peaceful_nature": [
"relaxed",
"lax",
"quiet",
"bashful",
"calm"
],
// The aggression multiplier for natures above.
"peaceful_nature_multiplier": -1.0,
// Pokemon with these natures are much more peaceful.
"more_peaceful_nature": [
"docile",
"timid",
"gentle",
"careful"
],
// The aggression multiplier for natures above.
"more_peaceful_nature_multiplier": -2.0,
// Pokemon won't be aggressive in these biomes
"peaceful_biome": [
"minecraft:deep_dark"
],
// Pokemon will be neutral in these biomes
"neutral_biome": [],
// Pokemon will always be aggressive in these biomes
"aggressive_biome": [],
// Forms that will always be aggressive. Cobblemon Survival: cobblesurvival_enraged marks a Pokémon that a failed catch or a
// weaker trainer angered (companion mod, catching/Catching.kt), so it fights instead of fleeing.
"always_aggro_aspects": [
"alolan",
"cobblesurvival_enraged"
],
// Pokemon that will always be aggressive
"always_aggro": [
"mankey",
"primeape"
],
// Pokemon that will never be aggressive, priority over always aggresive species/forms
"never_aggro": [
"slowpoke",
"pyukumuku"
],
// Pokemon that will be aggressive only when provoked(WIP)
"provoke_only_aggro": [],
// Pokemon that will always flee away from the player, priority over always aggresive species/forms
"always_flee": [
"wimpod"
],
// Abilities that will reduce wild pokemon's aggro
"aggro_reducing_abilities": [
"intimidate",
"unnerve",
"pressure"
],
// Abilities that works like Mold Breaker(unused)
"mold_breaker_like_ablilities": [
"moldbreaker",
"turboblaze",
"teravolt"
],
// Whitelist for all pokemon.(won't influence the Poke Staff)
"all_pokemon_targeting_whitelist": [
"minecraft:villager"
],
// Whitelist for wild pokemon.
"wild_pokemon_targeting_whitelist": [],
// Whitelist for player owned pokemon.(won't influence the Poke Staff)
"player_owned_pokemon_targeting_whitelist": [],
// Allow the Pokemon to use the teleport move to flee if the Pokemon had learnt it?
"allow_teleport_to_flee": true,
// Do player Pokemon defend their owners when they attack or are attacked by other mobs?
"do_pokemon_defend_owner": true,
// Do player Pokemon defend their owners proactively? (follows the same rules as Iron Golems!!!)
"do_pokemon_defend_proactive": true,
// Can player Pokemon target other players?
"do_player_pokemon_attack_other_players": false,
// Can player Pokemon target other player's Pokemon?
"do_player_pokemon_attack_other_player_pokemon": false,
// Will the wild pokemon cries for several times when angered,set to false so the pokemon will only cry one time when it's angered
"multiple_cries": true,
// Tick(1/20s by default) needed for the pokemon to cry again(it will only work when the multiple_cries is set to true)
"time_to_cry_again": 100,
// How much experience a pokemon can get by killing a pokemon without a battle? Set to 0 for no experience outside standard pokemon battles.
"experience_multiplier": 0.5,
// Your pokemon can gain EV points by killing a pokemon without a battle?
"can_gain_ev": true,
// The minimum EXP. player's Pokemon can gain from killing a Pokemon without battle
"min_xp_from_pokemon": 0,
// The minimum EXP. player's Pokemon can gain from killing a Pokemon without battle. If this value is lower than zero, there will be no limit.
"max_xp_from_pokemon": -1,
// If you want all the player's Pokemon that dealt the damage to gain EXP. and EV points
"pokemon_share_yield": true,
// If the Pokemon can evolve by using the move out of a Pokemon Battle? For example Primeape can use Rage Fist 20x to evolve without a traditional Pokemon Battle.
"can_progress_use_move_evolution": true,
// Pokemons should be immune to suffocation damage? (cant die to wall damage, like when sand drops into them)
"suffocation_immunity": true,
// Should player-owned pokemons be immune to damage from all players?
"pvp_immunity": true,
// Should player-owned pokemons be immune to damage from mobs on the same team?
"friendly_fire_immunity_team": true,
// Should player-owned pokemons be immune to damage from the owner (friendly fire)?
"friendly_fire_immunity_owner": true,
// The maximum damage bonus can get if it reached the maximum stat(the physical and the special moves both share one value currently)
"max_bonus_from_stat": 4.0,
// The minimum damage wild pokemon could do on hit
"minimum_attack_damage": 1.0,
// The maximum damage wild pokemon could do on hit
"maximum_attack_damage": 50.0,
// Minimum damage multiplier player-owned pokemon would do
"minimum_attack_damage_player": 1.2000000476837158,
// Damage multiplier player-owned pokemon would do
"attack_damage_player": 1.2000000476837158,
// Maximum damage multiplier player-owned pokemon would do
"maximum_attack_damage_player": 1.2000000476837158,
// Attack stat required to reach the maximum damage,the default value is calculated with 50 level, 130 stat, 252 EVs, IVs of 31, and a helpful nature.
"maximum_attack_stat": 200,
// The movement speed multiplier of a pokemon if the Speed stat of this Pokemon is 0.
"minimum_movement_speed": 1.2999999523162842,
// The movement speed multiplier of a pokemon if the Speed stat of this Pokemon reaches the value in the config.
"maximum_movement_speed": 2.0,
// The speed stat required for a pokemon to reach the highest fleeing and pursuing speed.The default value(548) is the max speed stat of a lvl.100 Regieleki with a beneficial nature.
"speed_stat_limit": 548,
// Use the classic type effect after hitting the enemy.(Levitate for the psychic type move,regeneration for grass type move)
"activate_type_effect": false,
// Use the move effect after hitting the enemy(Inferno will always burn the target, Flamethrower has a 10% chance to burn the target.)
"activate_move_effect": true,
// The maximum damage reduction wild pokemon can get from its defense/special defense(uses the highest one)
"max_damage_reduction_multiplier": 0.4000000059604645,
// The multiplier for player-owned Pokemon for maximum damage reduction
"max_damage_reduction_multiplier_player": 1.2000000476837158,
// The highest defense stat needed to get the highest damage reduction.
"defense_stat_limit": 161,
// When a player hurts wild pokemon, should a pokemon battle be started?
"force_wild_battle_on_player_attack": false,
// When a player hurts another player's pokemon, should a pokemon battle be started?
"force_player_battle_on_player_attack": false,
// When a player owned Pokemon hurts or is hurt by a wild pokemon, should a pokemon battle be started?
"force_wild_battle_on_pokemon_hurt": false,
// When a player owned Pokemon hurts or is hurt by another player's pokemon, should a pokemon battle be started?
"force_player_battle_on_pokemon_hurt": false,
// When a player is hurt by a wild pokemon, should a pokemon battle be started?
"force_wild_battle_on_player_hurt": false,
// When a player is hurt by another player's pokemon, should a pokemon battle be started?
"force_player_battle_on_player_hurt": false,
// If this is set to true, only the players of different teams can start a battle
"force_player_battle_check_team": true,
// If wild pokemon can use move to attack. If this is set to false, the move power will be set to the value of base_power in fightorflight_moves.json5 when calculating damage.
"wild_pokemon_can_use_move": false,
// If wild pokemon can use move to attack. If this is set to false, the move power will be set to the value of base_power in fightorflight_moves.json5 when calculating damage.
"wild_alpha_can_use_move": true,
// If wild alpha pokemon can use the range attack. If the pokemon can't use a move, it will only use range attack if the Special Attack is higher than Attack.
"wild_alpha_ranged_attack": true,
// If alpha Pokemon can change the move it use.
"wild_alpha_switch_move": true,
// The interval between the wild alpha pokemon tries to change its move. In seconds.
"wild_alpha_switch_move_interval": 5,
// If a wild alpha pokemon can use range attack and the gap between two Attack stat is greater than 30, it will try to use the moves that suit the highest one, or it will try to use a mix of them. If this is set to false, the alpha will choose move randomly.
"wild_alpha_switch_move_stat_check": true,
// If wild pokemon can use the range attack. If the pokemon can't use a move, it will only use range attack if the Special Attack is higher than Attack.
"wild_pokemon_ranged_attack": false,
// The minimum time between pokemons ranged attacks. In seconds.
"minimum_ranged_attack_interval": 1.0,
// The maximum time between pokemons ranged attacks. In seconds.
"maximum_ranged_attack_interval": 3.0,
// The minimum time between pokemons melee attacks. In seconds.
"minimum_melee_attack_interval": 1.0,
// The maximum time between pokemons melee attacks. In seconds.
"maximum_melee_attack_interval": 3.0,
// Minimum damage wild pokemon would do with ranged attacks
"minimum_ranged_attack_damage": 1.0,
// The basic duration of a move. In ticks
"base_move_duration": 20,
// Maximum damage wild pokemon would do with ranged attacks
"maximum_ranged_attack_damage": 50.0,
// Minimum damage multiplier player-owned pokemon would do with ranged attacks
"minimum_ranged_attack_damage_player": 1.2000000476837158,
// Damage multiplier player-owned pokemon would do with ranged attacks
"ranged_attack_damage_player": 1.2000000476837158,
// Maximum damage multiplier player-owned pokemon would do with ranged attacks
"maximum_ranged_attack_damage_player": 1.2000000476837158,
// Special attack stat required to reach the maximum damage,the default value is calculated with 50 level, 130 stat, 252 EVs, IVs of 31, and a helpful nature.
"maximum_special_attack_stat": 200,
// If the pokemon type effectiveness will be activated outside standard Pokemon battle
"type_effectiveness_between_pokemon": true,
// Multiplier for super effective damage against the other Pokemon
"super_effective_multiplier": 2.0,
// Multiplier for not very effective damage against the other Pokemon
"not_very_effective_multiplier": 0.5,
// Multiplier for no effect damage against the other Pokemon
"no_effect_multiplier": 0.10000000149011612,
// Water type damage will be more effective on mobs like Blaze, Enderman,etc.
"water_type_super_effective_dmg_multiplier": 2.0,
// Fire type damage will be not very effective against fire immune entity.(set to 0 if you want a complete immune)
"fire_type_no_effect_dmg_multiplier": 0.10000000149011612,
// Ice type damage will be not very effective against entity that can't be frozen.(set to 0 if you want a complete immune)
"ice_type_no_effect_dmg_multiplier": 0.10000000149011612,
// Ice type damage will be more effective against entity that has a weaker resistant to the frost damage.
"ice_type_super_effective_dmg_multiplier": 2.0,
// Poison type damage will be not very effective against undead mobs.(set to 0 if you want a complete immune)
"poison_type_no_effect_dmg_multiplier": 0.10000000149011612,
// If the Ender Dragon should have the Dragon Type weaknesses and resistances?
"ender_dragon_has_dragon_type": false,
// If you want to use FOF health mechanic. A different curve that was used in FOF previously. The following configurations needs this one to work.
"shouldOverrideHealthMechanic": true,
// If you want to use the FOF style hp calculation.
"use_fof_style_hp_calculation": true,
// If health sync will work on the wild pokemon, set to false will heal them on standard pokemon battle start if they were damaged before.
"enable_health_sync_for_wild_pokemon": false,
// The minimum hp of a pokemon outside standard pokemon battle,shedinja is set to 1.0 and can't be changed.
"min_HP": 8,
// The medium hp value of a pokemon outside standard pokemon battle,the medium value is designed to allow you to better tweak the growth of HP value for the entity
"mid_HP": 40,
// The maximum hp of a pokemon outside standard pokemon battle.
"max_HP": 100,
// Pokemon HP stat above this value will increase its HP outside standard pokemon battle.
"min_HP_required_stat": 20,
// HP stat needed to get medium HP outside standard pokemon battle.
"mid_HP_required_stat": 160,
// HP stat needed to get maximum HP outside standard pokemon battle. The max hp of a Blissey is 714.
"max_HP_required_stat": 500,
// If Poke Staff can be used.
"can_use_poke_staff": true,
// The Pokemon stays still after finishing the move command.
"stay_after_move_command": true,
// If the player need to hold a Poke Staff to use the command keybind
"should_check_poke_staff": true,
// If the Auto Battle command should be enabled
"enable_auto_battle_command": true,
// The friendship requirement for your Pokemon to obey the Auto Battle command. If this value is lower than zero, the friendship value won't influence the Pokemon.
"auto_battle_friendship_requirement": 100,
// If this is set to true, Pokemon with Auto Battle command won't attack Pokemon with a higher level.
"auto_battle_weaker_pokemon_only": true,
// If the pokemon can use held item(This config has the highest priority)
"can_use_held_item": true,
// If the pokemon can use damage-increasing held item
"can_use_held_item_damage_influencing": true,
// If the pokemon can use hp-restoring/damage-inflicting held item(Life Orb not included)
"can_use_held_item_hp_influencing": true,
// If the player's pokemon can hurt ender dragon
"player_pokemon_can_hurt_ender_dragon": true,
// If the wild poekmon can hurt ender dragon
"wild_pokemon_can_hurt_ender_dragon": false,
// If you want to use the FOF style melee attack?
"use_fof_style_melee": true,
// If you want to use range attack from FOF
"use_range_attack": true,
// If you want to set willDefendOwner to true in Pokemon's combat behaviour of Cobblemon, should be useful for solving the problem that player's Pokemon added by the datapacks can't attack.
"force_enable_defend_owner": true,
// If you want to set willDefendSelf to true in Pokemon's combat behaviour of Cobblemon, should be useful for solving the problem that wild Pokemon added by the datapacks can't attack.
"force_enable_defend_self": true,
// If you want to set willFlee to true in Pokemon's combat behaviour of Cobblemon. Will only be enabled to Pokemon that won't attack.(Not aggressive enough/not allowed to attack by the config.
"force_enable_flee": true,
// Required for attack_in_peaceful_difficulty. I override the original function so this is designed to avoid the conflicts with the other mods that needs to edit this function.
"use_fof_switch_to_fight": true,
// Required for attack_in_peaceful_difficulty, too. I override the original function so this is designed to avoid the conflicts with the other mods that needs to edit this function.
"use_fof_attack_angry_at": true,
// If you want to set willFlee to true in Pokemon's combat behaviour of Cobblemon. Will only be enabled to Pokemon that won't attack.(Not aggressive enough/not allowed to attack by the config.
"attack_in_peaceful_difficulty": true,
// Sizes that will be recognized as S for FOF when you are using the basic sizer.(The default sizer)(Deprecated)
"size_S_basic_sizer": [
"Tiny",
"Small"
],
// Sizes that will be recognized as M for FOF when you are using the basic sizer.(The default sizer)(Deprecated)
"size_M_basic_sizer": [
"Average"
],
// Sizes that will be recognized as L for FOF when you are using the basic sizer.(The default sizer)(Deprecated)
"size_L_basic_sizer": [
"Big",
"Large",
"Huge"
],
// Sizes that will be recognized as S for FOF when you are using the Gen9 sizer.(Deprecated)
"size_S_gen9_sizer": [
"S",
"XS",
"XXS",
"XXXS"
],
// Sizes that will be recognized as M for FOF when you are using the Gen9 sizer.(Deprecated)
"size_M_gen9_sizer": [
"M"
],
// Sizes that will be recognized as L for FOF when you are using the Gen9 sizer.(Deprecated)
"size_L_gen9_sizer": [
"L",
"XL",
"XXL",
"XXXL"
],
// Sizes that will be recognized as L for FOF when you are using Cobblemon
"size_S_cobblemon_sizer": [
"S",
"XS"
],
// Sizes that will be recognized as L for FOF when you are using Cobblemon
"size_M_cobblemon_sizer": [
"L",
"XL"
],
// Sizes that will be recognized as L for FOF when you are using Cobblemon
"size_L_cobblemon_sizer": [
"L",
"XL"
],
// The extra aggression a Pokemon can get if it's size is S.
"aggression_size_S_value": -30.0,
// The extra aggression a Pokemon can get if it's size is M.
"aggression_size_M_value": 0.0,
// The extra aggression a Pokemon can get if it's size is L.
"aggression_size_L_value": 50.0,
// If you want to use the custom type effectiveness, it allows you to use datapack to add weakness/resistance to non-pokemon entity.
"use_custom_type_effectiveness": true
}
File diff suppressed because it is too large. Load diff
+5 -1
View File
@@ -24,6 +24,10 @@ hash = "84f1468a01fcd0f0f728446c386cbb0d4b3df0a14883fa6da9f4f70e4519ba80"
file = "config/defaultoptions/keybindings.txt" file = "config/defaultoptions/keybindings.txt"
hash = "fbc8a858d5cfe978ed48a6a5f564b9d0a7b540e12b5f481c4eb797021a9f6078" hash = "fbc8a858d5cfe978ed48a6a5f564b9d0a7b540e12b5f481c4eb797021a9f6078"
[[files]]
file = "config/fightorflight.json5"
hash = "d1f8daadc8e34c41950e8f439ca089e8820142e57767459cbe1eb2a250ee4441"
[[files]] [[files]]
file = "config/lootr-common.toml" file = "config/lootr-common.toml"
hash = "ca348d022ad97e74ec51a896060e8fcfc079cf0d34ef116a967e72f60bdb3381" hash = "ca348d022ad97e74ec51a896060e8fcfc079cf0d34ef116a967e72f60bdb3381"
@@ -50,7 +54,7 @@ hash = "a6e9d717e2f73e463b25b1eacfbff43304fd8993937d8c002f9629f24ebcc221"
[[files]] [[files]]
file = "config/regenerating_ore_veins/veins.json" file = "config/regenerating_ore_veins/veins.json"
hash = "ead417ded7b2867aadf6946c5f919b95fb2001d5207c1c39753872a2676a5d79" hash = "d68a080ec5b881d6af1c5f06fbc2ca0d275b8898fc251f96ad3c1471394a8bd3"
[[files]] [[files]]
file = "config/worldgen_editor/presets/cobblesurvival_island.json" file = "config/worldgen_editor/presets/cobblesurvival_island.json"
+1 -1
View File
@@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0"
[index] [index]
file = "index.toml" file = "index.toml"
hash-format = "sha256" hash-format = "sha256"
hash = "f13be586ab9aaa36fb1db267f406efd31c227e77f20ed6e971306c164e15757f" hash = "90997231a86493a8e7488c2c295b314d3bfcfcfc670fb3f7699e827054b5823c"
[versions] [versions]
minecraft = "1.21.1" minecraft = "1.21.1"
+22 -2
View File
@@ -4,8 +4,11 @@
Vanilla's overworld biome parameters come from Minecraft's own data generator Vanilla's overworld biome parameters come from Minecraft's own data generator
(run on the vanilla server jar that the NeoForge installer put in (run on the vanilla server jar that the NeoForge installer put in
build/server; no server is started). Every entry is sent to its target biome build/server; no server is started). Every entry is sent to its target biome
from world/biomes.json, the extra entries are appended, and the result becomes from world/biomes.json ("remap"), or cut into pieces along one climate
the inline biome list of WorldGen Editor's delegate in parameter ("split": each piece of the parameter's range goes to its own
biome; the parts no piece covers are dropped, so the biome around them takes
over), the extra entries are appended ("add"), and the result becomes the
inline biome list of WorldGen Editor's delegate in
datapacks/core/data/cobblesurvival/worldgen/world_preset/island.json. datapacks/core/data/cobblesurvival/worldgen/world_preset/island.json.
The world type has no Nether dimension. The world type has no Nether dimension.
@@ -38,11 +41,28 @@ def vanilla_parameters():
return json.loads(report.read_text())["biomes"] return json.loads(report.read_text())["biomes"]
def split(entry, rule):
"""The pieces of a vanilla entry along rule["parameter"], each sent to its own biome."""
name = rule["parameter"]
value = entry["parameters"][name]
low, high = (value, value) if isinstance(value, (int, float)) else value
pieces = []
for piece in rule["pieces"]:
a, b = max(low, piece["range"][0]), min(high, piece["range"][1])
if a < b or (a == b and low == high):
pieces.append({"biome": piece["to"], "parameters": {**entry["parameters"], name: [a, b]}})
return pieces
def main(): def main():
spec = json.loads((ROOT / "world/biomes.json").read_text()) spec = json.loads((ROOT / "world/biomes.json").read_text())
targets = set(spec["allowed"]) | set(spec["water"]) targets = set(spec["allowed"]) | set(spec["water"])
entries, unmapped = [], set() entries, unmapped = [], set()
splits = {s["biome"]: s for s in spec.get("split", [])}
for entry in vanilla_parameters(): for entry in vanilla_parameters():
if entry["biome"] in splits:
entries += split(entry, splits[entry["biome"]])
continue
target = spec["remap"].get(entry["biome"]) target = spec["remap"].get(entry["biome"])
if target is None: if target is None:
unmapped.add(entry["biome"]) unmapped.add(entry["biome"])
+24 -50
View File
@@ -8,12 +8,11 @@
0,0 (the island plus a strip of sea; the sea corners inside the square 0,0 (the island plus a strip of sea; the sea corners inside the square
border generate when someone gets there). border generate when someone gets there).
3. In the same session: load the circle again row by row until Regenerating 3. In the same session: load the circle again row by row until Regenerating
Ore Veins has put veins into every chunk (it only fills a new chunk while Ore Veins has put its veins and the companion mod its ore nodes into every
it is loaded, 8 a tick, and Chunky is faster), place the showcase ore veins (world/zones.json → chunk (both only fill a new chunk while it is loaded, a few a tick, and
"ores", tiers with "showcase": one vein sticking out of the ground near Chunky is faster), save, measure where the land ends (tools/world_report.py)
its zone's sample point, so players see what a regenerating vein looks and set a square world border `border_margin` blocks outside the outermost
like), save, measure where the land ends (tools/world_report.py) and set coast.
a square world border `border_margin` blocks outside the outermost coast.
4. Copy the world to build/world-template without player data, then run 4. Copy the world to build/world-template without player data, then run
the world checks and render the map (build/island-map.png). the world checks and render the map (build/island-map.png).
@@ -36,14 +35,29 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent)) sys.path.insert(0, str(Path(__file__).resolve().parent))
from mcserver import BUILD, ROOT, SERVER, Server, chunky, eula_accepted, prepare, scan_log # noqa: E402 from mcserver import BUILD, ROOT, SERVER, Server, chunky, eula_accepted, prepare, scan_log # noqa: E402
import world_report # noqa: E402 import world_report # noqa: E402
import zone_grid # noqa: E402
from numpy import hypot as np_hypot # noqa: E402 from numpy import hypot as np_hypot # noqa: E402
TEMPLATE = BUILD / "world-template" TEMPLATE = BUILD / "world-template"
SKIP = shutil.ignore_patterns("playerdata", "advancements", "stats", "session.lock") SKIP = shutil.ignore_patterns("playerdata", "advancements", "stats", "session.lock")
# A showcase sits this far from its zone's sample point (where guardians and the self-tests stand), first offset
# that is still inside the zone wins.
SHOWCASE_OFFSETS = [(16, 0), (0, 16), (-16, 0), (0, -16), (24, 24), (-24, -24)] def border_for(world, recipe):
"""Square border around the land, `border_margin` past the outermost coast."""
data = world_report.analyse(world)
land = world_report.island_mask(data)
zs, xs = land.nonzero()
ox, oz = data["origin"]
x0, x1 = int(xs.min()) + ox, int(xs.max()) + ox + 1
z0, z1 = int(zs.min()) + oz, int(zs.max()) + oz + 1
far = float(np_hypot(xs + ox, zs + oz).max())
if far + recipe["border_margin"] > recipe["pregen_radius"]:
sys.exit(f"land reaches {far:.0f} blocks from 0,0; raise pregen_radius ({recipe['pregen_radius']}) in "
f"world/world.json to at least land + border_margin")
cx, cz = round((x0 + x1) / 2), round((z0 + z1) / 2)
size = max(x1 - x0, z1 - z0) + 2 * recipe["border_margin"]
print(f"land spans x {x0}..{x1}, z {z0}..{z1}, at most {far:.0f} blocks from 0,0; "
f"border center {cx},{cz} size {size}")
return cx, cz, size
def fill_veins(server, radius): def fill_veins(server, radius):
@@ -72,45 +86,6 @@ def fill_veins(server, radius):
return left return left
def place_showcases(server):
"""One hand-placed vein per showcase tier, half sunk into the ground near the zone's sample point."""
plan = json.loads((ROOT / "world/zones.json").read_text())
grid = zone_grid.load()
samples = json.loads(zone_grid.ISLAND.read_text())["samples"]
for tier, spec in plan["ores"].items():
if not spec.get("showcase"):
continue
sx, _, sz = samples[spec["zone"]]
spot = next(((sx + dx, sz + dz) for dx, dz in SHOWCASE_OFFSETS if grid.zone_at(sx + dx, sz + dz) == spec["zone"]), None)
if spot is None:
raise RuntimeError(f"no showcase spot inside {spec['zone']} near its sample point {sx},{sz}")
x, z = spot
server.command(f"forceload add {x} {z}")
reply = server.command(f"execute in minecraft:overworld positioned {x} 0 {z} positioned over world_surface "
f"positioned ~ ~-1 ~ run regenerating_ore_veins place {tier}_showcase").strip()
server.command(f"forceload remove {x} {z}")
print(f" showcase {tier} at {x},{z}: {reply[:100]}")
def border_for(world, recipe):
"""Square border around the land, `border_margin` past the outermost coast."""
data = world_report.analyse(world)
land = world_report.island_mask(data)
zs, xs = land.nonzero()
ox, oz = data["origin"]
x0, x1 = int(xs.min()) + ox, int(xs.max()) + ox + 1
z0, z1 = int(zs.min()) + oz, int(zs.max()) + oz + 1
far = float(np_hypot(xs + ox, zs + oz).max())
if far + recipe["border_margin"] > recipe["pregen_radius"]:
sys.exit(f"land reaches {far:.0f} blocks from 0,0; raise pregen_radius ({recipe['pregen_radius']}) in "
f"world/world.json to at least land + border_margin")
cx, cz = round((x0 + x1) / 2), round((z0 + z1) / 2)
size = max(x1 - x0, z1 - z0) + 2 * recipe["border_margin"]
print(f"land spans x {x0}..{x1}, z {z0}..{z1}, at most {far:.0f} blocks from 0,0; "
f"border center {cx},{cz} size {size}")
return cx, cz, size
def main(): def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.parse_args() parser.parse_args()
@@ -133,7 +108,6 @@ def main():
seconds = server.pregenerate(0, 0, recipe["pregen_radius"], shape="circle") seconds = server.pregenerate(0, 0, recipe["pregen_radius"], shape="circle")
print(f"pregeneration finished in {seconds:.0f}s") print(f"pregeneration finished in {seconds:.0f}s")
fill_veins(server, recipe["pregen_radius"]) fill_veins(server, recipe["pregen_radius"])
place_showcases(server)
server.command("save-all flush") server.command("save-all flush")
cx, cz, size = border_for(world, recipe) cx, cz, size = border_for(world, recipe)
print(f" {server.command(f'worldborder center {cx} {cz}').strip()}") print(f" {server.command(f'worldborder center {cx} {cz}').strip()}")
+30 -9
View File
@@ -3,15 +3,17 @@
* Ore scan: every block in every stored chunk is checked. Ore ("*_ore", * Ore scan: every block in every stored chunk is checked. Ore ("*_ore",
ancient debris, raw ore blocks from large veins) is only allowed as part of ancient debris, raw ore blocks from large veins) is only allowed as part of
a regenerating vein (Regenerating Ore Veins' saved data), and each vein's a regenerating vein (Regenerating Ore Veins' saved data: the underground
blocks must lie in the zone of its ore tier (world/zones.json → "ores"). veins and the companion mod's ore nodes), and each vein's blocks must lie
in the zone of its ore tier (world/zones.json → "ores").
A surface tier's blocks must lie in its zone's grid cells (the mod checks A surface tier's blocks must lie in its zone's grid cells (the mod checks
every block against the zone's boxes; at a cave border a vein can reach every block against the zone's boxes; at a cave border a vein can reach
into the cave biome). A cave tier's blocks must lie within two biome cells into the cave biome). A cave tier's blocks must lie within two biome cells
(8 blocks) of its cave biome: the mod checks the biome only at a vein's (8 blocks) of its cave biome: the mod checks the biome only at a vein's
centre, and Minecraft blends biomes over a few blocks. Untracked ore fails centre, and Minecraft blends biomes over a few blocks. Untracked ore fails
the check, and so (except with --sample) do an ore tier without any vein the check, and so (except with --sample) do an ore tier without nodes, a
and a chunk the mod never put veins into. surface tier without underground veins, and a chunk that never got its
veins and nodes.
* Biome set: every biome stored in any chunk section (surface and caves) * Biome set: every biome stored in any chunk section (surface and caves)
must be listed in world/biomes.json ("allowed" or "water"). must be listed in world/biomes.json ("allowed" or "water").
* End access: at least one stronghold must start inside the world border. * End access: at least one stronghold must start inside the world border.
@@ -231,20 +233,37 @@ def read_veins(world):
return veins, filled return veins, filled
def read_node_chunks(world):
"""The chunks the companion mod has put its ore nodes into (ore/OreNodes.kt)."""
path = Path(world, "data/cobblesurvival_ore_nodes.dat")
if not path.exists():
return set()
import gzip
data = anvil.parse_nbt(gzip.decompress(path.read_bytes()))
done = set()
for packed in data.get("data", data).get("done", []):
packed = int(packed)
x, z = packed & 0xFFFFFFFF, (packed >> 32) & 0xFFFFFFFF
done.add((x - (1 << 32) if x >= 1 << 31 else x, z - (1 << 32) if z >= 1 << 31 else z))
return done
def check_veins(world, ore_at, full_chunks, sample): def check_veins(world, ore_at, full_chunks, sample):
"""Every ore block belongs to a tracked vein in its tier's zone; returns (blocks per tier and zone, problems).""" """Every ore block belongs to a tracked vein in its tier's zone; returns (blocks per tier and zone, problems)."""
plan = json.loads((ROOT / "world/zones.json").read_text()) plan = json.loads((ROOT / "world/zones.json").read_text())
grid = zone_grid.load() grid = zone_grid.load()
cave_biome = {c["id"]: c["biome"] for c in plan["caves"]} cave_biome = {c["id"]: c["biome"] for c in plan["caves"]}
veins, filled = read_veins(world) veins, filled = read_veins(world)
per_tier, untracked, stray = {}, {}, {} nodes_done = read_node_chunks(world)
per_tier, untracked, stray, kinds = {}, {}, {}, set()
for x, y, z, name, biome, near_caves in ore_at: for x, y, z, name, biome, near_caves in ore_at:
vein = veins.get((x, y, z)) vein = veins.get((x, y, z))
if vein is None: if vein is None:
if ORE.search(name): # amethyst outside a vein is a natural geode if ORE.search(name): # amethyst outside a vein is a natural geode
untracked[name] = untracked.get(name, 0) + 1 untracked[name] = untracked.get(name, 0) + 1
continue continue
tier = vein.removesuffix("_showcase").removesuffix("_deep") tier = vein.removesuffix("_node").removesuffix("_deep")
kinds.add((tier, "node" if vein.endswith("_node") else "vein"))
zone = grid.zone_at(x, z, biome) or "none" zone = grid.zone_at(x, z, biome) or "none"
counts = per_tier.setdefault(tier, {}) counts = per_tier.setdefault(tier, {})
counts[zone] = counts.get(zone, 0) + 1 counts[zone] = counts.get(zone, 0) + 1
@@ -257,10 +276,12 @@ def check_veins(world, ore_at, full_chunks, sample):
stray.setdefault((vein, zone, want), []).append((x, y, z)) stray.setdefault((vein, zone, want), []).append((x, y, z))
problems = [f"{n} {name} outside any regenerating vein" for name, n in sorted(untracked.items())] problems = [f"{n} {name} outside any regenerating vein" for name, n in sorted(untracked.items())]
if not sample: if not sample:
problems += [f"ore tier {tier} has no vein in the world" for tier in plan["ores"] if tier not in per_tier] problems += [f"ore tier {tier} has no ore nodes in the world" for tier in plan["ores"] if (tier, "node") not in kinds]
unfilled = [c for c in full_chunks if c not in filled] problems += [f"ore tier {tier} has no underground veins in the world" for tier, spec in plan["ores"].items()
if "veins" in spec and (tier, "vein") not in kinds]
unfilled = [c for c in full_chunks if c not in filled or c not in nodes_done]
if unfilled: if unfilled:
problems.append(f"{len(unfilled)} chunks never got their ore veins (e.g. chunk {unfilled[0]})") problems.append(f"{len(unfilled)} chunks never got their ore veins and nodes (e.g. chunk {unfilled[0]})")
problems += [f"{len(at)} blocks of vein {vein} in zone {zone}, not {want or '(no tier)'} (e.g. {at[0]})" problems += [f"{len(at)} blocks of vein {vein} in zone {zone}, not {want or '(no tier)'} (e.g. {at[0]})"
for (vein, zone, want), at in sorted(stray.items(), key=str)] for (vein, zone, want), at in sorted(stray.items(), key=str)]
return per_tier, problems return per_tier, problems
+52 -33
View File
@@ -1,25 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Write Regenerating Ore Veins' config from the zone plan, so every ore tier only has veins in its zone. """Write Regenerating Ore Veins' config and the companion mod's ore node table from the zone plan.
world/zones.json → "ores" lists the tiers. This tool writes pack/config/regenerating_ore_veins/: world/zones.json → "ores" lists the tiers (Adrian, 2026-10-30: small ore nodes on the surface, larger veins
underground that are harder to find, and the cave zones' ores as nodes on the cave floor). This tool writes
pack/config/regenerating_ore_veins/:
* areas.json: each surface zone that has a tier, as boxes from the zone grid (build/zone-layout.json, * areas.json: each surface zone that has a tier, as boxes from the zone grid (build/zone-layout.json,
24-block cells; rows of cells merged into rectangles), named <zone>_<n>, reaching from bedrock to 24-block cells; rows of cells merged into rectangles), named <zone>_<n>, reaching from bedrock to
the build limit. The mod's boxes are centre plus half-size, so a box covers its cells except their the build limit. The mod's boxes are centre plus half-size, so a box covers its cells except their
last row and column of blocks: no box reaches into a neighbouring zone. last row and column of blocks: no box reaches into a neighbouring zone.
* veins.json: one vein per tier, or two when the tier has "stone" and "deepslate" blocks (an upper vein * veins.json: a surface tier's underground veins ("veins"): one vein, or two when the tier has "stone" and
from y 0 up, "<tier>_deep" below). A surface tier's veins are limited to its zone's boxes and kept out "deepslate" blocks (an upper vein from y 0 up, "<tier>_deep" below), limited to its zone's boxes and kept
of the cave biomes, which are zones of their own; a cave tier's veins to its cave biome. The mod checks out of the cave biomes, which are zones of their own. The mod checks the area for every block and the
the area for every block and the biome at a vein's centre. One vein only for the self-tests comes biome at a vein's centre. Then every tier's "<tier>_node": the ore node (the stone ore, or the tier's
last ("selftest", a single coal block); its area lies far outside the world border, so it never blocks), placed only by the companion mod, so its area lies far outside the world border, where the mod
generates, and the tests place it by command. Each tier with "showcase" also gets "<tier>_showcase", a full never generates it by itself. The same goes for "selftest" (a single coal block the self-tests place).
radius-2 vein in the same place-only area, which tools/make_world.py places by hand near the zone's sample point.
* global.json: regenerators can't be broken by players (allow_breaking false), so nobody can remove a * global.json: regenerators can't be broken by players (allow_breaking false), so nobody can remove a
vein for good; explosions don't destroy veins. vein for good; explosions don't destroy veins.
* vein_locator.json: the Vein Locator from a compass and a copper ingot (mid-game) instead of netherite. * vein_locator.json: the Vein Locator from a compass and a copper ingot (mid-game) instead of netherite.
It also writes world/vein-sources.json, the veins as item sources for the field guide (tools/fieldguide/build.py It also writes datapacks/core/data/cobblesurvival/ore_nodes/island.json, the node table the companion mod reads
reads it by default): what each tier's blocks drop and in which zone. (ore/OreNodes.kt: which node, in which zone, how often per chunk, on the surface or on a cave floor), and
world/vein-sources.json, the nodes and veins as item sources for the field guide (tools/fieldguide/build.py reads it
by default): what each tier's blocks drop and in which zone.
Re-run after changing the zone plan or the grid (tools/zone_layout.py) and commit the result. Re-run after changing the zone plan or the grid (tools/zone_layout.py) and commit the result.
@@ -35,6 +38,7 @@ LAYOUT = ROOT / "build/zone-layout.json"
BOTTOM, TOP = -64, 320 BOTTOM, TOP = -64, 320
SELFTEST_AREA = "selftest_nowhere" SELFTEST_AREA = "selftest_nowhere"
SOURCES = ROOT / "world/vein-sources.json" SOURCES = ROOT / "world/vein-sources.json"
NODES = ROOT / "datapacks/core/data/cobblesurvival/ore_nodes/island.json"
# What an ore block drops, where it isn't the block itself or "<name>_ore" → "<name>". # What an ore block drops, where it isn't the block itself or "<name>_ore" → "<name>".
DROPS = {"iron": "raw_iron", "copper": "raw_copper", "gold": "raw_gold", "lapis": "lapis_lazuli", DROPS = {"iron": "raw_iron", "copper": "raw_copper", "gold": "raw_gold", "lapis": "lapis_lazuli",
"nether_quartz": "quartz", "amethyst_block": "amethyst_block", "budding_amethyst": "amethyst_shard"} "nether_quartz": "quartz", "amethyst_block": "amethyst_block", "budding_amethyst": "amethyst_shard"}
@@ -83,7 +87,8 @@ def zone_areas(layout, zone_id):
return areas return areas
def vein(vein_id, blocks, spec, defaults, y_range, where): def vein(vein_id, blocks, spec, shape, defaults, y_range, where):
"""One entry of veins.json: spec gives the regrowth time, shape the size and spacing."""
seconds = spec["regen_minutes"] * 60 seconds = spec["regen_minutes"] * 60
jitter = round(seconds * spec.get("jitter", defaults["jitter"])) jitter = round(seconds * spec.get("jitter", defaults["jitter"]))
return { return {
@@ -93,13 +98,13 @@ def vein(vein_id, blocks, spec, defaults, y_range, where):
"dimension_whitelist": ["minecraft:overworld"], "dimension_whitelist": ["minecraft:overworld"],
**where, **where,
"shape": "sphere", "shape": "sphere",
"min_radius": spec["radius"][0], "min_radius": shape["radius"][0],
"max_radius": spec["radius"][1], "max_radius": shape["radius"][1],
"attempts": spec.get("attempts", defaults["attempts"]), "attempts": shape.get("attempts", defaults["attempts"]),
"min_y": y_range[0], "min_y": y_range[0],
"max_y": y_range[1], "max_y": y_range[1],
"chunk_minimum_generation_separation": spec["spacing"], "chunk_minimum_generation_separation": shape.get("spacing", 64),
"fill_factor": spec.get("fill", defaults["fill"]), "fill_factor": shape.get("fill", defaults["fill"]),
"regeneration_interval_seconds": seconds, "regeneration_interval_seconds": seconds,
"regeneration_interval_jitter": {"range_min": -jitter, "range_max": jitter}, "regeneration_interval_jitter": {"range_min": -jitter, "range_max": jitter},
} }
@@ -122,12 +127,24 @@ def main():
caves = {c["id"]: c["biome"] for c in plan["caves"]} caves = {c["id"]: c["biome"] for c in plan["caves"]}
surface = {z["id"] for z in plan["zones"]} surface = {z["id"] for z in plan["zones"]}
areas, area_names, veins = [], {}, [] areas, area_names, veins, nodes = [], {}, [], []
place_only = {"area_whitelist": [SELFTEST_AREA]}
for tier, spec in plan["ores"].items(): for tier, spec in plan["ores"].items():
zone = spec["zone"] zone = spec["zone"]
if zone not in caves and zone not in surface:
sys.exit(f"ore tier {tier}: unknown zone {zone}")
# A node is the stone ore; deepslate where there is no stone ore or the node sits below y 0 (deep caves).
deep = not spec.get("stone") or (zone in caves and spec["nodes"]["y"][1] < 0)
node_blocks = [tuple(b) for b in spec["blocks"]] if "blocks" in spec else [(spec["deepslate" if deep else "stone"], 1)]
node_shape = {**spec["nodes"], "fill": 1.0 if spec["nodes"]["radius"][1] == 0 else defaults["node_fill"]}
veins.append(vein(f"{tier}_node", node_blocks, spec, node_shape, defaults, (0, 0), place_only))
node = {"vein": f"{tier}_node", "zone": zone, "where": "cave" if zone in caves else "surface",
"chance": spec["nodes"]["chance"], "radius": spec["nodes"]["radius"][1]}
if zone in caves: if zone in caves:
where = {"biome_whitelist": [caves[zone]]} node.update({"biome": caves[zone], "tries": spec["nodes"]["tries"], "y": spec["nodes"]["y"]})
elif zone in surface: nodes.append(node)
if "veins" not in spec:
continue
if zone not in area_names: if zone not in area_names:
boxes = zone_areas(layout, zone) boxes = zone_areas(layout, zone)
if not boxes: if not boxes:
@@ -135,21 +152,16 @@ def main():
areas += boxes areas += boxes
area_names[zone] = [a["name"] for a in boxes] area_names[zone] = [a["name"] for a in boxes]
where = {"area_whitelist": area_names[zone], "biome_blacklist": sorted(caves.values())} where = {"area_whitelist": area_names[zone], "biome_blacklist": sorted(caves.values())}
else: shape = spec["veins"]
sys.exit(f"ore tier {tier}: unknown zone {zone}") low, high = shape["y"]
low, high = spec["y"]
if spec.get("showcase"):
blocks = [(spec["stone"], 1)] if spec.get("stone") else [tuple(b) for b in spec["blocks"]]
showcase = vein(f"{tier}_showcase", blocks, spec, defaults, (0, 0), {"area_whitelist": [SELFTEST_AREA]})
veins.append({**showcase, "min_radius": 2, "max_radius": 2, "fill_factor": 0.8})
if "blocks" in spec: if "blocks" in spec:
veins.append(vein(tier, spec["blocks"], spec, defaults, (low, high), where)) veins.append(vein(tier, spec["blocks"], spec, shape, defaults, (low, high), where))
continue continue
if spec.get("stone") and high >= 0: if spec.get("stone") and high >= 0:
veins.append(vein(tier, [(spec["stone"], 1)], spec, defaults, (max(low, 0), high), where)) veins.append(vein(tier, [(spec["stone"], 1)], spec, shape, defaults, (max(low, 0), high), where))
if spec.get("deepslate") and low < 0: if spec.get("deepslate") and low < 0:
deep_id = f"{tier}_deep" if spec.get("stone") and high >= 0 else tier deep_id = f"{tier}_deep" if spec.get("stone") and high >= 0 else tier
veins.append(vein(deep_id, [(spec["deepslate"], 1)], spec, defaults, (low, min(high, -1)), where)) veins.append(vein(deep_id, [(spec["deepslate"], 1)], spec, shape, defaults, (low, min(high, -1)), where))
areas.append({"dimension": "minecraft:overworld", "name": SELFTEST_AREA, "type": "box", areas.append({"dimension": "minecraft:overworld", "name": SELFTEST_AREA, "type": "box",
"x": 29_000_000, "y": 0, "z": 29_000_000, "dimx": 0, "dimy": 0, "dimz": 0}) "x": 29_000_000, "y": 0, "z": 29_000_000, "dimx": 0, "dimy": 0, "dimz": 0})
@@ -177,12 +189,18 @@ def main():
"durability": 2, "durability": 2,
"stack_size": 64, "stack_size": 64,
}, indent=2) + "\n") }, indent=2) + "\n")
NODES.parent.mkdir(parents=True, exist_ok=True)
NODES.write_text(json.dumps({
"_comment": "Generated by tools/zone_ores.py from world/zones.json; read by the companion mod (ore/OreNodes.kt).",
"nodes": nodes}, indent=1) + "\n")
names = {z["id"]: z["name"] for z in plan["zones"] + plan["caves"]} names = {z["id"]: z["name"] for z in plan["zones"] + plan["caves"]}
sources = [] sources = []
for tier, spec in plan["ores"].items(): for tier, spec in plan["ores"].items():
blocks = [b for b, _ in spec.get("blocks", [])] + [spec[k] for k in ("stone", "deepslate") if spec.get(k)] blocks = [b for b, _ in spec.get("blocks", [])] + [spec[k] for k in ("stone", "deepslate") if spec.get(k)]
where = (f"ore nodes on the cave floor in {names[spec['zone']]}" if spec["zone"] in caves
else f"ore nodes on the ground and veins underground in {names[spec['zone']]}")
for item in dict.fromkeys(drop_of(b) for b in blocks): for item in dict.fromkeys(drop_of(b) for b in blocks):
sources.append({"item": item, "how": f"Regenerating {tier.replace('_', ' ')} vein in {names[spec['zone']]}", sources.append({"item": item, "how": f"Regenerating {tier.replace('_', ' ')} {where}",
"kind": "ore vein", "needs": []}) "kind": "ore vein", "needs": []})
SOURCES.write_text(json.dumps({ SOURCES.write_text(json.dumps({
"_comment": "Generated by tools/zone_ores.py from world/zones.json; extra item sources for the field guide.", "_comment": "Generated by tools/zone_ores.py from world/zones.json; extra item sources for the field guide.",
@@ -190,10 +208,11 @@ def main():
per_zone = {} per_zone = {}
for tier, spec in plan["ores"].items(): for tier, spec in plan["ores"].items():
per_zone.setdefault(spec["zone"], []).append(tier) per_zone.setdefault(spec["zone"], []).append(tier)
print(f"wrote {len(veins)} veins ({len(plan['ores'])} tiers) and {len(areas)} areas to {OUT.relative_to(ROOT)}") print(f"wrote {len(veins)} veins ({len(plan['ores'])} tiers) and {len(areas)} areas to {OUT.relative_to(ROOT)}, "
f"{len(nodes)} node kinds to {NODES.relative_to(ROOT)}")
for zone, tiers in per_zone.items(): for zone, tiers in per_zone.items():
boxes = len(area_names.get(zone, [])) boxes = len(area_names.get(zone, []))
print(f" {zone}: {', '.join(tiers)}" + (f" ({boxes} boxes)" if boxes else " (cave biome)")) print(f" {zone}: {', '.join(tiers)}" + (f" ({boxes} boxes)" if boxes else " (cave nodes)"))
if __name__ == "__main__": if __name__ == "__main__":
+11
View File
@@ -79,6 +79,17 @@
"minecraft:dripstone_caves": "minecraft:dripstone_caves", "minecraft:dripstone_caves": "minecraft:dripstone_caves",
"minecraft:deep_dark": "minecraft:deep_dark" "minecraft:deep_dark": "minecraft:deep_dark"
}, },
"split": [
{
"_comment": "Adrian (2026-10-30): the Deep Dark took the island's whole deepslate layer. Vanilla puts it at depth 1.1 wherever erosion is low, at any temperature. Now it is the Deep Dark only under cold and temperate land (vanilla's three cooler temperature bands); under the hottest land (desert and badlands climate) it becomes basalt deltas (the Basalt Furnace, a cave zone up to Lv 70); under warm land, ordinary caves of the land above.",
"biome": "minecraft:deep_dark",
"parameter": "temperature",
"pieces": [
{"range": [-1.0, -0.1], "to": "minecraft:deep_dark"},
{"range": [0.4, 1.0], "to": "minecraft:basalt_deltas"}
]
}
],
"add": [ "add": [
{ {
"_comment": "Volcanic caves under the hottest, driest land (where desert and badlands are), at cave depth like lush and dripstone caves.", "_comment": "Volcanic caves under the hottest, driest land (where desert and badlands are), at cave depth like lush and dripstone caves.",
+22 -22
View File
@@ -3,133 +3,133 @@
"sources": [ "sources": [
{ {
"item": "minecraft:coal", "item": "minecraft:coal",
"how": "Regenerating coal vein in Button Isle", "how": "Regenerating coal ore nodes on the ground and veins underground in Button Isle",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:raw_copper", "item": "minecraft:raw_copper",
"how": "Regenerating copper vein in Southwold", "how": "Regenerating copper ore nodes on the ground and veins underground in Southwold",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:raw_iron", "item": "minecraft:raw_iron",
"how": "Regenerating iron vein in Westmarch", "how": "Regenerating iron ore nodes on the ground and veins underground in Westmarch",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:redstone", "item": "minecraft:redstone",
"how": "Regenerating redstone vein in Verdant Jungle", "how": "Regenerating redstone ore nodes on the ground and veins underground in Verdant Jungle",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:lapis_lazuli", "item": "minecraft:lapis_lazuli",
"how": "Regenerating lapis vein in Highland Meadows", "how": "Regenerating lapis ore nodes on the ground and veins underground in Highland Meadows",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:raw_gold", "item": "minecraft:raw_gold",
"how": "Regenerating gold vein in Sunscorch", "how": "Regenerating gold ore nodes on the ground and veins underground in Sunscorch",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:emerald", "item": "minecraft:emerald",
"how": "Regenerating emerald vein in Frostpeak Range", "how": "Regenerating emerald ore nodes on the ground and veins underground in Frostpeak Range",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:amethyst_block", "item": "minecraft:amethyst_block",
"how": "Regenerating amethyst vein in Dripstone Depths", "how": "Regenerating amethyst ore nodes on the cave floor in Dripstone Depths",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:amethyst_shard", "item": "minecraft:amethyst_shard",
"how": "Regenerating amethyst vein in Dripstone Depths", "how": "Regenerating amethyst ore nodes on the cave floor in Dripstone Depths",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:diamond", "item": "minecraft:diamond",
"how": "Regenerating diamond vein in Deep Dark", "how": "Regenerating diamond ore nodes on the cave floor in Deep Dark",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:quartz", "item": "minecraft:quartz",
"how": "Regenerating quartz vein in Basalt Furnace", "how": "Regenerating quartz ore nodes on the cave floor in Basalt Furnace",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "minecraft:ancient_debris", "item": "minecraft:ancient_debris",
"how": "Regenerating netherite vein in Basalt Furnace", "how": "Regenerating netherite ore nodes on the cave floor in Basalt Furnace",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:thunder_stone", "item": "cobblemon:thunder_stone",
"how": "Regenerating thunder stone vein in Westmarch", "how": "Regenerating thunder stone ore nodes on the ground and veins underground in Westmarch",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:shiny_stone", "item": "cobblemon:shiny_stone",
"how": "Regenerating shiny stone vein in Lush Caves", "how": "Regenerating shiny stone ore nodes on the cave floor in Lush Caves",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:leaf_stone", "item": "cobblemon:leaf_stone",
"how": "Regenerating leaf stone vein in Verdant Jungle", "how": "Regenerating leaf stone ore nodes on the ground and veins underground in Verdant Jungle",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:water_stone", "item": "cobblemon:water_stone",
"how": "Regenerating water stone vein in Mangrove Mire", "how": "Regenerating water stone ore nodes on the ground and veins underground in Mangrove Mire",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:moon_stone", "item": "cobblemon:moon_stone",
"how": "Regenerating moon stone vein in Dripstone Depths", "how": "Regenerating moon stone ore nodes on the cave floor in Dripstone Depths",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:sun_stone", "item": "cobblemon:sun_stone",
"how": "Regenerating sun stone vein in Highland Meadows", "how": "Regenerating sun stone ore nodes on the ground and veins underground in Highland Meadows",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:fire_stone", "item": "cobblemon:fire_stone",
"how": "Regenerating fire stone vein in Sunscorch", "how": "Regenerating fire stone ore nodes on the ground and veins underground in Sunscorch",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:ice_stone", "item": "cobblemon:ice_stone",
"how": "Regenerating ice stone vein in Frostpeak Range", "how": "Regenerating ice stone ore nodes on the ground and veins underground in Frostpeak Range",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:dawn_stone", "item": "cobblemon:dawn_stone",
"how": "Regenerating dawn stone vein in Frostpeak Range", "how": "Regenerating dawn stone ore nodes on the ground and veins underground in Frostpeak Range",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
}, },
{ {
"item": "cobblemon:dusk_stone", "item": "cobblemon:dusk_stone",
"how": "Regenerating dusk stone vein in Deep Dark", "how": "Regenerating dusk stone ore nodes on the cave floor in Deep Dark",
"kind": "ore vein", "kind": "ore vein",
"needs": [] "needs": []
} }
+27 -27
View File
@@ -1,32 +1,32 @@
{ {
"_comment": "Zone plan for Phase 3 (model approved by Adrian 2026-10-29; levels are placeholders, see docs/balance.md). tools/zone_layout.py assigns each surface cell of the island to the first zone whose rule matches, in this order, and renders build/zone-map.png. Rules: 'within' = distance from 0,0 in blocks; 'biomes' = cells of these biomes, clusters smaller than min_cells dropped, plus 'absorb' = cells of other biomes within that many blocks of the cluster; 'region' = what is left, by half-plane (x/z bounds in blocks); 'water' = sea cells of these biomes. Biomes in 'inherit' (rivers) take the zone of the land around them. Cave zones follow the cave biome everywhere under the island and are not drawn. 'lines' = how many evolution lines tools/zone_spawns.py aims to give the zone (scaled to the lines available); 'species' pins a line to a zone by hand. 'guardian': the zone's Alpha guardian (tools/zone_spawns.py picks the strongest Pokémon spawning in the zone unless 'species' is given, at the band's top level + 5) and its drops, which help with the next zone; it stands at the zone's sample point and returns guardian_respawn_hours after it is beaten (Adrian, 2026-10-29: fixed spots, 5 hours). 'ores': Phase 4 ore tiers for Regenerating Ore Veins (tools/zone_ores.py writes the mod's config from them). Each tier has veins in exactly one zone, higher tiers in higher zones; Cobblemon's evolution stones are tiers too, since their natural ores are off. 'stone'/'deepslate' split a tier into an upper vein (y >= 0) and a lower one; 'blocks' is a weighted list for one vein. 'radius' in blocks, 'y' range, 'spacing' in chunks between candidate chunks, 'regen_minutes' of real time, plus 'jitter' (share of the interval), 'fill' and 'attempts' from 'ore_defaults'. Cave tiers' 'y' follows where their biome lies (the biome preview: Deep Dark mostly below y -24, the other three between 0 and 40), and they get more 'attempts', since each try lands at a random height. 'showcase': the template gets one hand-placed vein of the tier near its zone's sample point (vein '<tier>_showcase': radius 2, never generates by itself). 'level_pressure': a Pokémon more than grace_levels above the band of the zone its trainer stands in gets per_level less experience per extra level, down to minimum (Exp. Candy and commands excepted).", "_comment": "Zone plan for Phase 3 (model approved by Adrian 2026-10-29; levels are placeholders, see docs/balance.md). tools/zone_layout.py assigns each surface cell of the island to the first zone whose rule matches, in this order, and renders build/zone-map.png. Rules: 'within' = distance from 0,0 in blocks; 'biomes' = cells of these biomes, clusters smaller than min_cells dropped, plus 'absorb' = cells of other biomes within that many blocks of the cluster; 'region' = what is left, by half-plane (x/z bounds in blocks); 'water' = sea cells of these biomes. Biomes in 'inherit' (rivers) take the zone of the land around them. Cave zones follow the cave biome everywhere under the island and are not drawn. 'lines' = how many evolution lines tools/zone_spawns.py aims to give the zone (scaled to the lines available); 'species' pins a line to a zone by hand. 'guardian': the zone's Alpha guardian (tools/zone_spawns.py picks the strongest Pokémon spawning in the zone unless 'species' is given, at the band's top level + 5) and its drops, which help with the next zone; it stands at the zone's sample point and returns guardian_respawn_hours after it is beaten (Adrian, 2026-10-29: fixed spots, 5 hours). 'ores': Phase 4 ore tiers (tools/zone_ores.py writes Regenerating Ore Veins' config and the companion mod's node table from them). Each tier lives in exactly one zone, higher tiers in higher zones; Cobblemon's evolution stones are tiers too. Adrian (2026-10-30): small nodes on the surface, larger veins underground that are harder to find, and the cave zones' ores as nodes on the cave floor, not in the walls. 'nodes': 'chance' per chunk, 'radius' in blocks (0 = one block); cave tiers also 'tries' (columns searched per chunk) and 'y' (where their cave biome lies). 'veins' (surface tiers only, underground): 'radius', 'y', 'spacing' in chunks between candidate chunks, 'attempts'; 'stone'/'deepslate' split them into an upper vein (y >= 0) and a lower one, 'blocks' is a weighted list. 'regen_minutes' of real time, plus 'jitter', 'fill' (veins) and 'node_fill' (nodes bigger than one block) from 'ore_defaults'. 'level_pressure': a Pokémon more than grace_levels above the band of the zone its trainer stands in gets per_level less experience per extra level, down to minimum (Exp. Candy and commands excepted).",
"min_cells": 6, "min_cells": 6,
"inherit": ["minecraft:river"], "inherit": ["minecraft:river"],
"guardian_respawn_hours": 5, "guardian_respawn_hours": 5,
"level_pressure": {"grace_levels": 5, "per_level": 0.1, "minimum": 0.1}, "level_pressure": {"grace_levels": 5, "per_level": 0.1, "minimum": 0.1},
"ore_defaults": {"fill": 0.7, "attempts": 1, "jitter": 0.1}, "ore_defaults": {"fill": 0.7, "node_fill": 0.8, "attempts": 1, "jitter": 0.1},
"ores": { "ores": {
"coal": {"zone": "button_isle", "stone": "minecraft:coal_ore", "deepslate": "minecraft:deepslate_coal_ore", "radius": [1, 2], "y": [-32, 100], "spacing": 4, "regen_minutes": 20, "attempts": 2, "showcase": true}, "coal": {"zone": "button_isle", "stone": "minecraft:coal_ore", "deepslate": "minecraft:deepslate_coal_ore", "regen_minutes": 20, "nodes": {"chance": 0.5, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-32, 100], "spacing": 6, "attempts": 2}},
"copper": {"zone": "southwold", "stone": "minecraft:copper_ore", "deepslate": "minecraft:deepslate_copper_ore", "radius": [1, 2], "y": [-32, 100], "spacing": 5, "regen_minutes": 30, "attempts": 2, "showcase": true}, "copper": {"zone": "southwold", "stone": "minecraft:copper_ore", "deepslate": "minecraft:deepslate_copper_ore", "regen_minutes": 30, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-32, 100], "spacing": 8, "attempts": 2}},
"iron": {"zone": "westmarch", "stone": "minecraft:iron_ore", "deepslate": "minecraft:deepslate_iron_ore", "radius": [1, 2], "y": [-48, 90], "spacing": 5, "regen_minutes": 40, "attempts": 2, "showcase": true}, "iron": {"zone": "westmarch", "stone": "minecraft:iron_ore", "deepslate": "minecraft:deepslate_iron_ore", "regen_minutes": 40, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-48, 90], "spacing": 8, "attempts": 2}},
"redstone": {"zone": "jungle", "stone": "minecraft:redstone_ore", "deepslate": "minecraft:deepslate_redstone_ore", "radius": [1, 2], "y": [-56, 60], "spacing": 6, "regen_minutes": 45, "showcase": true}, "redstone": {"zone": "jungle", "stone": "minecraft:redstone_ore", "deepslate": "minecraft:deepslate_redstone_ore", "regen_minutes": 45, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-56, 60], "spacing": 9, "attempts": 1}},
"lapis": {"zone": "highlands", "stone": "minecraft:lapis_ore", "deepslate": "minecraft:deepslate_lapis_ore", "radius": [1, 2], "y": [-56, 80], "spacing": 6, "regen_minutes": 45, "showcase": true}, "lapis": {"zone": "highlands", "stone": "minecraft:lapis_ore", "deepslate": "minecraft:deepslate_lapis_ore", "regen_minutes": 45, "nodes": {"chance": 0.15, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-56, 80], "spacing": 9, "attempts": 1}},
"gold": {"zone": "sunscorch", "stone": "minecraft:gold_ore", "deepslate": "minecraft:deepslate_gold_ore", "radius": [1, 2], "y": [-48, 120], "spacing": 5, "regen_minutes": 60, "showcase": true, "attempts": 2}, "gold": {"zone": "sunscorch", "stone": "minecraft:gold_ore", "deepslate": "minecraft:deepslate_gold_ore", "regen_minutes": 60, "nodes": {"chance": 0.3, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-48, 120], "spacing": 7, "attempts": 2}},
"emerald": {"zone": "frostpeak", "stone": "minecraft:emerald_ore", "deepslate": "minecraft:deepslate_emerald_ore", "radius": [1, 1], "y": [-16, 320], "spacing": 6, "regen_minutes": 75, "showcase": true, "attempts": 2}, "emerald": {"zone": "frostpeak", "stone": "minecraft:emerald_ore", "deepslate": "minecraft:deepslate_emerald_ore", "regen_minutes": 75, "nodes": {"chance": 0.15, "radius": [0, 1]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 8, "attempts": 2}},
"amethyst": {"zone": "dripstone", "blocks": [["minecraft:amethyst_block", 3], ["minecraft:budding_amethyst", 1]], "radius": [1, 1], "y": [0, 40], "spacing": 10, "regen_minutes": 120, "attempts": 4}, "amethyst": {"zone": "dripstone", "blocks": [["minecraft:amethyst_block", 3], ["minecraft:budding_amethyst", 1]], "regen_minutes": 120, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [0, 40]}},
"diamond": {"zone": "deep_dark", "deepslate": "minecraft:deepslate_diamond_ore", "radius": [1, 1], "y": [-64, -24], "spacing": 10, "regen_minutes": 90, "attempts": 3}, "diamond": {"zone": "deep_dark", "deepslate": "minecraft:deepslate_diamond_ore", "regen_minutes": 90, "nodes": {"chance": 0.15, "tries": 8, "radius": [1, 1], "y": [-64, -24]}},
"quartz": {"zone": "basalt", "blocks": [["minecraft:nether_quartz_ore", 1]], "radius": [1, 2], "y": [0, 40], "spacing": 8, "regen_minutes": 60, "attempts": 3}, "quartz": {"zone": "basalt", "blocks": [["minecraft:nether_quartz_ore", 1]], "regen_minutes": 60, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [0, 40]}},
"netherite": {"zone": "basalt", "blocks": [["minecraft:ancient_debris", 1]], "radius": [0, 1], "y": [0, 40], "spacing": 14, "regen_minutes": 180, "attempts": 2}, "netherite": {"zone": "basalt", "blocks": [["minecraft:ancient_debris", 1]], "regen_minutes": 180, "nodes": {"chance": 0.06, "tries": 8, "radius": [0, 0], "y": [0, 40]}},
"thunder_stone": {"zone": "westmarch", "stone": "cobblemon:thunder_stone_ore", "deepslate": "cobblemon:deepslate_thunder_stone_ore", "radius": [0, 1], "y": [-48, 100], "spacing": 8, "regen_minutes": 60}, "thunder_stone": {"zone": "westmarch", "stone": "cobblemon:thunder_stone_ore", "deepslate": "cobblemon:deepslate_thunder_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.03, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
"shiny_stone": {"zone": "lush_caves", "stone": "cobblemon:shiny_stone_ore", "deepslate": "cobblemon:deepslate_shiny_stone_ore", "radius": [0, 1], "y": [-16, 40], "spacing": 10, "regen_minutes": 60, "attempts": 4}, "shiny_stone": {"zone": "lush_caves", "stone": "cobblemon:shiny_stone_ore", "deepslate": "cobblemon:deepslate_shiny_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.15, "tries": 8, "radius": [0, 1], "y": [-16, 40]}},
"leaf_stone": {"zone": "jungle", "stone": "cobblemon:leaf_stone_ore", "deepslate": "cobblemon:deepslate_leaf_stone_ore", "radius": [0, 1], "y": [-48, 100], "spacing": 8, "regen_minutes": 60}, "leaf_stone": {"zone": "jungle", "stone": "cobblemon:leaf_stone_ore", "deepslate": "cobblemon:deepslate_leaf_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
"water_stone": {"zone": "mire", "stone": "cobblemon:water_stone_ore", "deepslate": "cobblemon:deepslate_water_stone_ore", "radius": [0, 1], "y": [-48, 100], "spacing": 8, "regen_minutes": 60, "showcase": true}, "water_stone": {"zone": "mire", "stone": "cobblemon:water_stone_ore", "deepslate": "cobblemon:deepslate_water_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
"moon_stone": {"zone": "dripstone", "blocks": [["cobblemon:dripstone_moon_stone_ore", 1], ["cobblemon:moon_stone_ore", 1], ["cobblemon:deepslate_moon_stone_ore", 1]], "radius": [0, 1], "y": [0, 40], "spacing": 10, "regen_minutes": 60, "attempts": 4}, "moon_stone": {"zone": "dripstone", "blocks": [["cobblemon:dripstone_moon_stone_ore", 1], ["cobblemon:moon_stone_ore", 1], ["cobblemon:deepslate_moon_stone_ore", 1]], "regen_minutes": 60, "nodes": {"chance": 0.25, "tries": 8, "radius": [0, 1], "y": [0, 40]}},
"sun_stone": {"zone": "highlands", "stone": "cobblemon:sun_stone_ore", "deepslate": "cobblemon:deepslate_sun_stone_ore", "radius": [0, 1], "y": [-48, 100], "spacing": 8, "regen_minutes": 60}, "sun_stone": {"zone": "highlands", "stone": "cobblemon:sun_stone_ore", "deepslate": "cobblemon:deepslate_sun_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.02, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
"fire_stone": {"zone": "sunscorch", "stone": "cobblemon:fire_stone_ore", "deepslate": "cobblemon:deepslate_fire_stone_ore", "radius": [0, 1], "y": [-48, 120], "spacing": 6, "regen_minutes": 60, "attempts": 2}, "fire_stone": {"zone": "sunscorch", "stone": "cobblemon:fire_stone_ore", "deepslate": "cobblemon:deepslate_fire_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.08, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 120], "spacing": 9, "attempts": 2}},
"ice_stone": {"zone": "frostpeak", "stone": "cobblemon:ice_stone_ore", "deepslate": "cobblemon:deepslate_ice_stone_ore", "radius": [0, 1], "y": [-16, 320], "spacing": 6, "regen_minutes": 60, "attempts": 2}, "ice_stone": {"zone": "frostpeak", "stone": "cobblemon:ice_stone_ore", "deepslate": "cobblemon:deepslate_ice_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.06, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 9, "attempts": 2}},
"dawn_stone": {"zone": "frostpeak", "stone": "cobblemon:dawn_stone_ore", "deepslate": "cobblemon:deepslate_dawn_stone_ore", "radius": [0, 1], "y": [-16, 320], "spacing": 7, "regen_minutes": 75, "attempts": 2}, "dawn_stone": {"zone": "frostpeak", "stone": "cobblemon:dawn_stone_ore", "deepslate": "cobblemon:deepslate_dawn_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 10, "attempts": 2}},
"dusk_stone": {"zone": "deep_dark", "deepslate": "cobblemon:deepslate_dusk_stone_ore", "radius": [0, 1], "y": [-64, -24], "spacing": 12, "regen_minutes": 75, "attempts": 3} "dusk_stone": {"zone": "deep_dark", "deepslate": "cobblemon:deepslate_dusk_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.1, "tries": 8, "radius": [0, 1], "y": [-64, -24]}}
}, },
"species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt"}, "species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt"},
"zones": [ "zones": [
@@ -72,9 +72,9 @@
} }
], ],
"caves": [ "caves": [
{"id": "lush_caves", "lines": 20, "name": "Lush Caves", "biome": "minecraft:lush_caves", "levels": [12, 28], "hazard": "none"}, {"id": "lush_caves", "types": ["grass", "bug", "fairy", "poison", "water"], "lines": 20, "name": "Lush Caves", "biome": "minecraft:lush_caves", "levels": [12, 28], "hazard": "none"},
{"id": "dripstone", "lines": 20, "name": "Dripstone Depths", "biome": "minecraft:dripstone_caves", "levels": [22, 38], "hazard": "none"}, {"id": "dripstone", "types": ["rock", "ground", "steel", "fairy", "psychic", "dragon"], "lines": 20, "name": "Dripstone Depths", "biome": "minecraft:dripstone_caves", "levels": [22, 38], "hazard": "none"},
{"id": "deep_dark", "guardian": {"drops": [["cobblemon:icy_rock", 3], ["cobblemon:exp_candy_l", 2]]}, "lines": 20, "name": "Deep Dark", "biome": "minecraft:deep_dark", "levels": [45, 60], "hazard": "darkness (vanilla sculk; no Warden)"}, {"id": "deep_dark", "types": ["ghost", "dark", "steel", "poison", "psychic", "rock"], "guardian": {"drops": [["cobblemon:icy_rock", 3], ["cobblemon:exp_candy_l", 2]]}, "lines": 20, "name": "Deep Dark", "biome": "minecraft:deep_dark", "levels": [45, 60], "hazard": "darkness (vanilla sculk; no Warden)"},
{"id": "basalt", "lines": 15, "name": "Basalt Furnace", "biome": "minecraft:basalt_deltas", "levels": [55, 70], "hazard": "heat (Cold Sweat)"} {"id": "basalt", "types": ["fire", "rock", "ground", "steel", "dragon", "dark"], "lines": 15, "name": "Basalt Furnace", "biome": "minecraft:basalt_deltas", "levels": [55, 70], "hazard": "heat (Cold Sweat)"}
] ]
} }