From 6361636da73b3db425cfb70b9c04b4c861b36b67 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 30 Oct 2026 15:44:17 +0100 Subject: [PATCH] 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 --- README.md | 4 +- .../data/cobblesurvival/catching/rules.json | 11 + .../data/cobblesurvival/ore_nodes/island.json | 194 +++++++ .../tags/block/ore_node_ground.json | 23 + .../worldgen/world_preset/island.json | 3 +- .../data/cobblesurvival/zones/island.json | 2 +- docs/balance.md | 6 +- docs/checklists/phase-4.md | 5 +- docs/design.md | 24 +- .../dev/cobblesurvival/CobbleSurvival.kt | 5 + .../dev/cobblesurvival/catching/Catching.kt | 113 ++++ .../cobblesurvival/catching/FightOrFlight.kt | 33 ++ .../kotlin/dev/cobblesurvival/dev/OreVeins.kt | 43 +- .../kotlin/dev/cobblesurvival/ore/OreNodes.kt | 201 +++++++ .../cobblesurvival/selftest/CatchingTests.kt | 90 ++++ .../cobblesurvival/selftest/OreNodeTests.kt | 54 ++ .../dev/cobblesurvival/selftest/SelfTest.kt | 6 +- .../dev/cobblesurvival/world/OnlyPokemon.kt | 32 +- pack/config/fightorflight.json5 | 382 +++++++++++++ pack/config/regenerating_ore_veins/veins.json | 506 +++++++++++------- pack/index.toml | 6 +- pack/pack.toml | 2 +- tools/biome_layout.py | 24 +- tools/make_world.py | 74 +-- tools/world_report.py | 39 +- tools/zone_ores.py | 99 ++-- world/biomes.json | 11 + world/vein-sources.json | 44 +- world/zones.json | 54 +- 29 files changed, 1733 insertions(+), 357 deletions(-) create mode 100644 datapacks/core/data/cobblesurvival/catching/rules.json create mode 100644 datapacks/core/data/cobblesurvival/ore_nodes/island.json create mode 100644 datapacks/core/data/cobblesurvival/tags/block/ore_node_ground.json create mode 100644 mod/src/main/kotlin/dev/cobblesurvival/catching/Catching.kt create mode 100644 mod/src/main/kotlin/dev/cobblesurvival/catching/FightOrFlight.kt create mode 100644 mod/src/main/kotlin/dev/cobblesurvival/ore/OreNodes.kt create mode 100644 mod/src/main/kotlin/dev/cobblesurvival/selftest/CatchingTests.kt create mode 100644 mod/src/main/kotlin/dev/cobblesurvival/selftest/OreNodeTests.kt create mode 100644 pack/config/fightorflight.json5 diff --git a/README.md b/README.md index df08b7f..7e9cf7a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Only the companion mod's in-game tests, on a fresh flat world (faster while work ```bash 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 python3 tools/zone_layout.py @@ -72,7 +72,7 @@ Writes each zone's spawn pool from Cobblemon's spawn data (every evolution line ```bash 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 python3 tools/drop_audit.py diff --git a/datapacks/core/data/cobblesurvival/catching/rules.json b/datapacks/core/data/cobblesurvival/catching/rules.json new file mode 100644 index 0000000..506b3c1 --- /dev/null +++ b/datapacks/core/data/cobblesurvival/catching/rules.json @@ -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 +} diff --git a/datapacks/core/data/cobblesurvival/ore_nodes/island.json b/datapacks/core/data/cobblesurvival/ore_nodes/island.json new file mode 100644 index 0000000..6781ee7 --- /dev/null +++ b/datapacks/core/data/cobblesurvival/ore_nodes/island.json @@ -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 + ] + } + ] +} diff --git a/datapacks/core/data/cobblesurvival/tags/block/ore_node_ground.json b/datapacks/core/data/cobblesurvival/tags/block/ore_node_ground.json new file mode 100644 index 0000000..a8f6d92 --- /dev/null +++ b/datapacks/core/data/cobblesurvival/tags/block/ore_node_ground.json @@ -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" + ] +} diff --git a/datapacks/core/data/cobblesurvival/worldgen/world_preset/island.json b/datapacks/core/data/cobblesurvival/worldgen/world_preset/island.json index ea0a6b4..b910628 100644 --- a/datapacks/core/data/cobblesurvival/worldgen/world_preset/island.json +++ b/datapacks/core/data/cobblesurvival/worldgen/world_preset/island.json @@ -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: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: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}} ] } diff --git a/datapacks/core/data/cobblesurvival/zones/island.json b/datapacks/core/data/cobblesurvival/zones/island.json index 4a554cf..b0d49de 100644 --- a/datapacks/core/data/cobblesurvival/zones/island.json +++ b/datapacks/core/data/cobblesurvival/zones/island.json @@ -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],[8,9],[5,6],[4,1],[5,9],[1,2],[5,7],[1,2],[5,1],[1,1],[5,15],[2,9],[5,1],[8,5],[9,7],[8,5],[4,6],[6,46],[9,10],[8,10],[9,1],[5,3],[4,3],[5,8],[1,1],[5,8],[1,2],[5,2],[1,1],[5,13],[2,11],[8,6],[9,5],[8,6],[4,6],[6,45],[9,10],[0,1],[8,10],[9,2],[5,1],[4,5],[5,3],[4,3],[5,1],[1,1],[5,7],[1,3],[5,14],[2,12],[5,1],[8,6],[9,5],[8,6],[4,7],[6,43],[9,7],[8,1],[0,9],[8,4],[9,3],[8,1],[4,6],[5,1],[4,4],[5,1],[1,1],[5,6],[1,2],[5,14],[2,1],[5,1],[2,11],[5,2],[8,6],[9,5],[8,5],[4,8],[6,42],[9,6],[8,1],[0,13],[9,4],[8,3],[4,9],[5,2],[1,2],[5,2],[1,2],[5,16],[2,14],[5,2],[8,6],[9,5],[8,5],[4,8],[6,41],[8,1],[9,5],[8,1],[0,15],[9,2],[8,4],[4,9],[5,2],[1,5],[5,2],[4,2],[5,10],[2,17],[5,3],[8,5],[9,5],[8,5],[6,1],[4,7],[6,12],[3,2],[6,27],[9,5],[8,1],[0,17],[8,6],[4,8],[5,3],[1,3],[5,2],[4,5],[5,5],[2,19],[5,4],[8,5],[9,5],[8,5],[6,1],[4,6],[6,12],[3,3],[6,27],[9,4],[8,1],[0,19],[8,5],[4,9],[5,2],[1,3],[5,1],[4,6],[5,6],[2,18],[5,4],[8,6],[9,4],[8,5],[4,7],[6,9],[3,6],[6,26],[9,5],[0,20],[8,4],[9,2],[4,11],[1,1],[5,2],[4,6],[5,7],[2,16],[5,5],[8,6],[9,4],[8,5],[4,6],[6,9],[3,6],[6,27],[9,4],[8,1],[0,21],[9,5],[4,21],[5,6],[2,15],[5,7],[8,5],[9,4],[8,5],[4,5],[6,10],[3,5],[6,27],[8,1],[9,4],[0,22],[9,5],[4,22],[5,6],[2,13],[5,8],[8,5],[9,8],[8,5],[9,39],[8,4],[9,5],[0,23],[9,5],[8,1],[9,3],[8,42],[5,2],[8,5],[9,8],[8,4],[9,49],[0,23],[9,1],[8,3],[9,6],[8,48],[9,8],[8,1],[9,51],[0,24],[8,5],[9,5],[8,48],[9,6],[8,3],[9,51],[8,1],[0,23],[8,5],[9,5],[8,48],[9,6],[8,3],[9,51],[8,1],[0,23],[8,5],[9,6],[8,47],[9,6],[8,3],[9,52],[0,23],[8,5],[9,6],[8,47],[9,6],[8,3],[7,4],[8,42],[7,1],[9,5],[0,23],[8,5],[4,4],[8,39],[4,4],[7,1],[8,5],[9,6],[8,3],[7,47],[8,1],[9,4],[0,22],[8,5],[4,48],[7,1],[8,5],[9,6],[8,3],[7,47],[8,1],[9,4],[8,1],[0,21],[9,1],[8,4],[4,34],[7,1],[4,8],[7,3],[4,2],[8,6],[9,6],[8,3],[7,48],[9,5],[8,1],[0,19],[9,2],[8,4],[4,42],[7,5],[4,1],[8,6],[9,6],[8,3],[7,49],[9,5],[0,19],[9,3],[8,2],[4,43],[7,5],[4,1],[8,5],[9,7],[8,4],[7,48],[9,6],[0,17],[9,5],[8,1],[4,42],[7,6],[4,1],[8,1],[9,1],[8,3],[9,7],[8,4],[7,49],[9,6],[0,15],[9,6],[7,1],[4,14],[7,2],[4,27],[7,5],[4,1],[8,1],[9,2],[8,2],[9,8],[8,3],[7,49],[9,7],[8,1],[0,11],[9,7],[7,2],[4,42],[7,6],[4,1],[9,4],[8,1],[9,8],[8,3],[7,50],[9,8],[8,1],[0,7],[9,8],[8,1],[7,4],[4,41],[7,5],[8,1],[9,13],[8,3],[7,51],[9,22],[8,1],[7,6],[4,41],[7,4],[8,1],[9,13],[8,4],[7,51],[8,1],[9,19],[7,8],[4,42],[7,2],[4,1],[8,1],[9,13],[8,4],[7,53],[9,17],[7,9],[4,43],[7,1],[4,1],[9,15],[8,3],[7,55],[9,13],[7,11],[4,44],[8,1],[9,15],[8,3],[7,58],[9,1],[8,1],[9,2],[8,1],[9,1],[8,1],[7,15],[4,42],[7,1],[8,1],[9,15],[8,4],[7,80],[4,38],[7,4],[8,1],[9,16],[8,3],[7,80],[4,36],[7,5],[8,1],[9,17],[8,3],[7,81],[4,1],[3,1],[4,32],[7,6],[8,1],[9,17],[8,4],[7,78],[3,3],[4,6],[3,1],[4,24],[7,8],[8,1],[9,18],[8,3],[7,78],[3,3],[4,6],[3,3],[4,21],[7,8],[8,1],[9,19],[8,3],[7,76],[3,5],[4,7],[3,3],[4,20],[7,8],[8,1],[9,19],[8,4],[7,71],[3,10],[4,6],[3,4],[4,19],[7,8],[8,1],[9,20],[8,3],[7,67],[3,15],[4,5],[3,5],[4,18],[7,7],[8,1],[9,21],[8,4],[7,65],[3,17],[4,4],[3,7],[4,16],[7,7],[8,1],[9,22],[8,3],[7,64],[3,32],[4,13],[7,6],[8,1],[9,23],[8,3],[7,63],[3,33],[4,13],[7,6],[8,1],[9,24],[8,3],[7,62],[3,35],[4,11],[7,5],[8,1],[9,25],[8,4],[7,61],[3,38],[4,7],[7,6],[8,1],[9,26],[8,3],[7,58],[3,44],[4,4],[7,5],[8,1],[9,27],[8,4],[7,57],[3,44],[4,3],[7,5],[8,2],[9,28],[8,3],[7,56],[3,45],[4,2],[7,6],[8,1],[9,29],[8,4],[7,55],[3,45],[7,7],[8,1],[9,31],[8,3],[7,55],[3,7],[7,3],[3,34],[7,8],[8,1],[9,32],[8,3],[7,54],[3,6],[7,4],[3,34],[7,7],[8,1],[9,33],[8,4],[7,53],[3,6],[7,3],[3,34],[7,7],[8,1],[9,35],[8,4],[7,52],[3,6],[7,2],[3,7],[7,3],[3,25],[7,7],[9,37],[8,3],[7,52],[3,15],[7,3],[3,27],[7,3],[8,1],[9,38],[8,4],[7,52],[3,16],[7,1],[3,27],[7,3],[8,1],[9,39],[8,4],[7,51],[3,43],[7,3],[8,1],[9,41],[8,4],[7,51],[3,43],[7,1],[8,1],[9,43],[8,4],[7,50],[3,43],[8,1],[9,4],[8,2],[9,39],[8,4],[7,50],[3,41],[8,7],[9,40],[8,5],[7,49],[3,30],[7,1],[3,9],[8,7],[9,42],[8,5],[7,47],[3,26],[7,7],[3,7],[8,7],[9,44],[8,5],[7,47],[3,24],[7,9],[3,5],[8,7],[9,46],[8,5],[7,46],[3,24],[7,7],[4,1],[3,5],[8,7],[9,48],[8,5],[7,45],[3,21],[7,9],[4,3],[3,2],[8,8],[9,50],[8,5],[7,44],[3,17],[7,13],[4,3],[3,1],[8,8],[9,53],[8,4],[7,43],[3,15],[7,14],[4,3],[3,1],[8,8],[9,55],[8,5],[7,37],[3,2],[7,1],[3,15],[7,16],[4,2],[8,8],[9,57],[8,6],[7,35],[3,15],[7,19],[4,1],[8,8],[9,59],[8,6],[7,34],[3,12],[7,21],[8,9],[9,62],[8,6],[7,32],[3,10],[7,22],[8,9],[9,64],[8,6],[7,32],[3,9],[7,20],[8,9],[9,68],[8,6],[7,33],[3,3],[7,21],[8,10],[9,70],[8,7],[7,54],[8,10],[9,73],[8,7],[7,50],[8,6],[9,1],[8,3],[9,77],[8,7],[7,45],[8,8],[9,2],[8,2],[9,79],[8,9],[7,29],[2,2],[7,8],[8,9],[9,87],[8,9],[7,23],[2,6],[7,6],[8,9],[9,91],[8,11],[7,19],[2,8],[7,1],[8,10],[9,96],[8,12],[7,1],[8,1],[7,7],[8,22],[9,101],[8,39],[9,106],[8,32],[9,115],[8,22],[9,628]]} +{"_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],[1,2],[5,7],[1,2],[5,1],[1,1],[5,15],[2,9],[8,1],[9,2],[8,3],[9,6],[8,4],[9,1],[4,7],[6,46],[9,5],[8,5],[9,2],[8,8],[9,1],[5,3],[4,3],[5,8],[1,1],[5,8],[1,2],[5,2],[1,1],[5,13],[2,10],[8,1],[9,4],[8,1],[9,6],[8,5],[4,7],[6,45],[9,7],[8,4],[9,1],[0,1],[8,8],[9,2],[5,1],[4,5],[5,3],[4,3],[5,1],[1,1],[5,7],[1,3],[5,14],[2,12],[8,1],[9,11],[8,5],[4,8],[6,43],[9,7],[8,1],[0,9],[8,4],[9,3],[8,1],[4,6],[5,1],[4,4],[5,1],[1,1],[5,6],[1,2],[5,14],[2,1],[5,1],[2,11],[5,2],[8,1],[9,10],[8,5],[4,8],[6,42],[9,6],[8,1],[0,13],[9,4],[8,3],[4,9],[5,2],[1,2],[5,2],[1,2],[5,16],[2,14],[5,2],[8,1],[9,10],[8,5],[4,8],[6,41],[8,1],[9,5],[8,1],[0,15],[9,2],[8,4],[4,9],[5,2],[1,5],[5,2],[4,2],[5,10],[2,17],[5,2],[8,1],[9,9],[8,6],[4,8],[6,12],[3,2],[6,27],[9,5],[8,1],[0,17],[8,6],[4,8],[5,3],[1,3],[5,2],[4,5],[5,5],[2,19],[5,3],[8,1],[9,9],[8,5],[6,1],[4,7],[6,12],[3,3],[6,27],[9,4],[8,1],[0,19],[8,5],[4,9],[5,2],[1,3],[5,1],[4,6],[5,6],[2,18],[5,3],[8,1],[9,9],[8,5],[4,8],[6,9],[3,6],[6,26],[9,5],[0,20],[8,6],[4,11],[1,1],[5,2],[4,6],[5,7],[2,16],[5,5],[9,9],[8,5],[4,7],[6,9],[3,6],[6,27],[9,4],[8,1],[0,21],[8,5],[4,21],[5,6],[2,15],[5,6],[9,9],[8,5],[4,6],[6,10],[3,5],[6,27],[8,1],[9,4],[0,22],[8,5],[4,22],[5,6],[2,13],[5,7],[8,1],[9,8],[8,1],[9,3],[8,1],[4,1],[8,1],[9,42],[8,4],[9,5],[0,23],[9,1],[8,2],[9,2],[8,1],[9,1],[8,7],[9,22],[8,15],[5,1],[8,5],[9,4],[8,1],[9,3],[8,1],[9,53],[0,23],[9,1],[8,3],[9,1],[8,15],[9,19],[8,18],[9,4],[8,1],[9,3],[8,2],[9,51],[0,24],[9,1],[8,26],[9,15],[8,15],[9,6],[8,4],[9,51],[8,1],[0,23],[9,2],[8,30],[9,13],[8,12],[9,6],[8,4],[9,51],[8,1],[0,23],[8,1],[9,1],[8,29],[9,14],[8,12],[9,6],[8,4],[9,46],[8,1],[9,5],[0,23],[8,30],[9,16],[8,11],[9,6],[8,3],[7,6],[8,41],[7,1],[9,5],[0,23],[8,5],[4,3],[8,22],[9,16],[8,2],[4,4],[8,5],[9,6],[8,4],[7,47],[8,1],[9,4],[0,22],[8,5],[4,48],[8,5],[9,6],[8,4],[7,47],[8,1],[9,4],[8,1],[0,21],[9,1],[8,4],[4,34],[7,1],[4,9],[7,2],[4,2],[8,5],[9,6],[8,3],[7,49],[9,5],[8,1],[0,19],[9,2],[8,4],[4,42],[7,6],[8,5],[9,6],[8,4],[7,49],[9,5],[0,19],[9,5],[4,43],[7,6],[8,5],[9,7],[8,3],[7,49],[9,6],[0,17],[9,6],[4,42],[7,6],[8,6],[9,7],[8,3],[7,50],[9,6],[0,15],[9,6],[7,1],[4,14],[7,2],[4,27],[7,5],[8,6],[9,7],[8,3],[7,50],[9,7],[8,1],[0,11],[9,7],[7,2],[4,42],[7,6],[8,5],[9,8],[8,3],[7,51],[9,8],[8,1],[0,7],[9,8],[7,5],[4,41],[7,5],[8,5],[9,8],[8,4],[7,51],[9,22],[8,1],[7,6],[4,41],[7,4],[8,5],[9,8],[8,4],[7,52],[8,1],[9,19],[7,8],[4,42],[7,2],[8,6],[9,9],[8,3],[7,54],[9,17],[7,9],[4,44],[8,6],[9,9],[8,3],[7,56],[9,13],[7,11],[4,44],[8,5],[9,10],[8,4],[7,58],[9,4],[8,1],[9,1],[8,1],[7,15],[4,42],[8,6],[9,11],[8,3],[7,81],[4,38],[7,3],[8,3],[9,14],[8,3],[7,81],[4,36],[7,5],[8,1],[9,16],[8,4],[7,81],[4,1],[3,1],[4,32],[7,5],[8,1],[9,18],[8,3],[7,79],[3,3],[4,6],[3,1],[4,24],[7,7],[8,1],[9,18],[8,3],[7,79],[3,3],[4,6],[3,3],[4,21],[7,8],[8,1],[9,18],[8,4],[7,76],[3,5],[4,7],[3,3],[4,20],[7,7],[8,1],[9,20],[8,3],[7,72],[3,10],[4,6],[3,4],[4,19],[7,7],[8,1],[9,20],[8,4],[7,67],[3,15],[4,5],[3,5],[4,18],[7,7],[9,22],[8,3],[7,66],[3,17],[4,4],[3,7],[4,16],[7,6],[8,1],[9,22],[8,4],[7,64],[3,32],[4,13],[7,6],[9,24],[8,3],[7,63],[3,33],[4,13],[7,5],[8,1],[9,24],[8,4],[7,62],[3,35],[4,11],[7,5],[9,26],[8,3],[7,62],[3,38],[4,7],[7,5],[8,1],[9,26],[8,4],[7,58],[3,44],[4,4],[7,4],[8,1],[9,28],[8,3],[7,58],[3,44],[4,3],[7,5],[8,1],[9,28],[8,4],[7,56],[3,45],[4,2],[7,5],[8,1],[9,30],[8,3],[7,56],[3,45],[7,7],[8,1],[9,31],[8,3],[7,55],[3,7],[7,3],[3,34],[7,7],[8,1],[9,32],[8,4],[7,54],[3,6],[7,4],[3,34],[7,7],[9,34],[8,3],[7,54],[3,6],[7,3],[3,34],[7,7],[8,1],[9,35],[8,3],[7,53],[3,6],[7,2],[3,7],[7,3],[3,25],[7,6],[8,1],[9,36],[8,4],[7,52],[3,15],[7,3],[3,27],[7,3],[8,1],[9,38],[8,4],[7,52],[3,16],[7,1],[3,27],[7,2],[8,1],[9,40],[8,3],[7,52],[3,43],[7,3],[8,1],[9,40],[8,4],[7,52],[3,43],[7,1],[8,1],[9,42],[8,4],[7,51],[3,43],[8,1],[9,44],[8,4],[7,51],[3,41],[8,7],[9,40],[8,4],[7,50],[3,30],[7,1],[3,9],[8,7],[9,42],[8,4],[7,48],[3,26],[7,7],[3,7],[8,7],[9,44],[8,4],[7,48],[3,24],[7,9],[3,5],[8,7],[9,46],[8,4],[7,47],[3,24],[7,7],[4,1],[3,5],[8,8],[9,47],[8,4],[7,46],[3,21],[7,9],[4,3],[3,2],[8,9],[9,49],[8,4],[7,45],[3,17],[7,13],[4,3],[3,1],[8,9],[9,51],[8,5],[7,43],[3,15],[7,14],[4,3],[3,1],[8,9],[9,53],[8,5],[7,38],[3,2],[7,1],[3,15],[7,16],[4,2],[8,8],[9,56],[8,6],[7,36],[3,15],[7,19],[4,1],[8,8],[9,59],[8,5],[7,35],[3,12],[7,21],[8,9],[9,61],[8,6],[7,33],[3,10],[7,22],[8,9],[9,63],[8,7],[7,32],[3,9],[7,20],[8,10],[9,66],[8,6],[7,34],[3,3],[7,21],[8,10],[9,69],[8,7],[7,55],[8,10],[9,72],[8,7],[7,51],[8,11],[9,75],[8,7],[7,47],[8,7],[9,2],[8,2],[9,79],[8,8],[7,41],[8,8],[9,4],[8,1],[9,81],[8,9],[7,26],[2,4],[7,6],[8,9],[9,90],[8,10],[7,21],[2,7],[7,3],[8,9],[9,95],[8,10],[7,14],[8,4],[2,2],[8,1],[2,2],[8,12],[9,99],[8,13],[7,8],[8,19],[9,105],[8,34],[9,111],[8,28],[9,118],[8,19],[9,488]]} diff --git a/docs/balance.md b/docs/balance.md index d0e2438..b9d9e0a 100644 --- a/docs/balance.md +++ b/docs/balance.md @@ -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`) | | | 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 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 | | 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`) | @@ -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) | | 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 | +| 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` | | | 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 | diff --git a/docs/checklists/phase-4.md b/docs/checklists/phase-4.md index 56fda22..25d5482 100644 --- a/docs/checklists/phase-4.md +++ b/docs/checklists/phase-4.md @@ -3,7 +3,7 @@ ## 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`. -- **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`. ## 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). -- [ ] 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. - [ ] 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). diff --git a/docs/design.md b/docs/design.md index 87a2590..8c3ca46 100644 --- a/docs/design.md +++ b/docs/design.md @@ -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. -**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). -- Tiers with an ordinary and a deepslate ore get two veins: the ordinary ore from y 0 up, the deepslate one below. +- **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. +- **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, ...). - 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. -- 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 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`). +- 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. +- 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`). **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 `, `... grant|revoke `. - 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`. ## Open decisions for Adrian (proceeding with the default, flagged) diff --git a/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt b/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt index 36034f4..7dabf86 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt @@ -3,8 +3,10 @@ package dev.cobblesurvival import com.cobblemon.mod.common.Cobblemon import com.cobblemon.mod.common.api.pokemon.PokemonSpecies import com.mojang.logging.LogUtils +import dev.cobblesurvival.catching.Catching import dev.cobblesurvival.dev.BiomePreview import dev.cobblesurvival.dev.OreVeins +import dev.cobblesurvival.ore.OreNodes import dev.cobblesurvival.permadeath.Permadeath import dev.cobblesurvival.progress.Gates import dev.cobblesurvival.progress.Progress @@ -58,11 +60,14 @@ class CobbleSurvival { NeoForge.EVENT_BUS.addListener(::onPortalSpawn) NeoForge.EVENT_BUS.addListener(OnlyPokemon::onChunkLoad) NeoForge.EVENT_BUS.addListener(OnlyPokemon::onEntityJoin) + NeoForge.EVENT_BUS.addListener(OnlyPokemon::onServerTick) Permadeath.register() ZoneCondition.register() NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners) Guardians.register() Gates.register() + OreNodes.register() + Catching.register() ZoneHazards.register() LevelPressure.register() SelfTest.register() diff --git a/mod/src/main/kotlin/dev/cobblesurvival/catching/Catching.kt b/mod/src/main/kotlin/dev/cobblesurvival/catching/Catching.kt new file mode 100644 index 0000000..8069578 --- /dev/null +++ b/mod/src/main/kotlin/dev/cobblesurvival/catching/Catching.kt @@ -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 { 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, 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) + } +} diff --git a/mod/src/main/kotlin/dev/cobblesurvival/catching/FightOrFlight.kt b/mod/src/main/kotlin/dev/cobblesurvival/catching/FightOrFlight.kt new file mode 100644 index 0000000..c7fb96a --- /dev/null +++ b/mod/src/main/kotlin/dev/cobblesurvival/catching/FightOrFlight.kt @@ -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() + } +} diff --git a/mod/src/main/kotlin/dev/cobblesurvival/dev/OreVeins.kt b/mod/src/main/kotlin/dev/cobblesurvival/dev/OreVeins.kt index 66f26a1..23e77dc 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/dev/OreVeins.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/dev/OreVeins.kt @@ -1,6 +1,7 @@ package dev.cobblesurvival.dev import com.mojang.brigadier.builder.LiteralArgumentBuilder +import dev.cobblesurvival.ore.OreNodes import net.minecraft.commands.CommandSourceStack import net.minecraft.commands.Commands 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.server.level.ColumnPos import net.minecraft.server.level.ServerLevel +import net.minecraft.util.RandomSource import net.minecraft.world.level.ChunkPos +import net.neoforged.fml.ModList +import java.util.Optional import kotlin.math.max import kotlin.math.min @@ -17,20 +21,47 @@ import kotlin.math.min * dependency of the companion mod). * * Dev tool: `/cobblesurvival veins unfilled ` 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 - * forgets the rest when the server stops, so a pregeneration run outpaces it; tools/make_world.py loads the island - * again row by row and waits until this count is 0 around each row (loading a row also fully loads the chunks next - * to it). The self-tests use [markMined] to wind a vein block's clock back. + * the mod hasn't put veins into yet, or the companion mod its ore nodes ([OreNodes]). Both fill a new chunk only + * while that chunk is loaded, a few chunks a tick, and the vein mod forgets the rest when the server stops, so a + * pregeneration run outpaces them; tools/make_world.py loads the island again row by row and waits until this count + * 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 { - 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 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 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 = 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). */ fun markMined(level: ServerLevel, pos: BlockPos, epochSecond: Long) { markMined.invoke(get.invoke(null, level), pos, epochSecond) @@ -46,7 +77,7 @@ object OreVeins { val level = context.source.server.overworld() val unfilled = chunks(ColumnPosArgument.getColumnPos(context, "from"), 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) 1 }))) diff --git a/mod/src/main/kotlin/dev/cobblesurvival/ore/OreNodes.kt b/mod/src/main/kotlin/dev/cobblesurvival/ore/OreNodes.kt new file mode 100644 index 0000000..2487617 --- /dev/null +++ b/mod/src/main/kotlin/dev/cobblesurvival/ore/OreNodes.kt @@ -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/.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?, + 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 = TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "ore_node_ground")) + + var nodes: List = emptyList() + private set + private val pending = LongOpenHashSet() + + fun register() { + NeoForge.EVENT_BUS.addListener { it.addListener(this) } + NeoForge.EVENT_BUS.addListener(::onChunkLoad) + NeoForge.EVENT_BUS.addListener(::onLevelTick) + NeoForge.EVENT_BUS.addListener { pending.clear() } + } + + override fun apply(files: Map, 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 { + val placed = ArrayList() + 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 { + 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"))) } + } +} diff --git a/mod/src/main/kotlin/dev/cobblesurvival/selftest/CatchingTests.kt b/mod/src/main/kotlin/dev/cobblesurvival/selftest/CatchingTests.kt new file mode 100644 index 0000000..f2ca6a5 --- /dev/null +++ b/mod/src/main/kotlin/dev/cobblesurvival/selftest/CatchingTests.kt @@ -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" } + } +} diff --git a/mod/src/main/kotlin/dev/cobblesurvival/selftest/OreNodeTests.kt b/mod/src/main/kotlin/dev/cobblesurvival/selftest/OreNodeTests.kt new file mode 100644 index 0000000..04a4fb1 --- /dev/null +++ b/mod/src/main/kotlin/dev/cobblesurvival/selftest/OreNodeTests.kt @@ -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() + } +} diff --git a/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt b/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt index cdc1b34..3ab07c7 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt @@ -2,6 +2,7 @@ package dev.cobblesurvival.selftest import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.cobblesurvival.CobbleSurvival +import dev.cobblesurvival.catching.Catching import net.minecraft.commands.CommandSourceStack import net.minecraft.commands.Commands import net.minecraft.core.BlockPos @@ -51,7 +52,8 @@ object SelfTest { private val cases: List get() = 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 = emptyList() private var forced: List = emptyList() @@ -99,6 +101,7 @@ object SelfTest { GameTestInfo(function, Rotation.NONE, level, RetryOptions.noRetries()).also { it.addListener(scenario) } } running = scenarios + Catching.hostilityPaused = true GameTestTicker.SINGLETON.clear() GameTestRunner.Builder.fromInfo(infos, level) .newStructureSpawner(StructureGridSpawner(origin, TESTS_PER_ROW, false)) @@ -128,5 +131,6 @@ object SelfTest { val level = event.server.overworld() forced.forEach { level.setChunkForced(it.x, it.z, false) } running = emptyList() + Catching.hostilityPaused = false } } diff --git a/mod/src/main/kotlin/dev/cobblesurvival/world/OnlyPokemon.kt b/mod/src/main/kotlin/dev/cobblesurvival/world/OnlyPokemon.kt index 1f50a14..6e2ffc6 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/world/OnlyPokemon.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/world/OnlyPokemon.kt @@ -9,20 +9,24 @@ import net.minecraft.server.level.ServerLevel import net.minecraft.tags.TagKey import net.minecraft.world.entity.EntityType 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.Blocks import net.minecraft.world.level.block.InfestedBlock import net.minecraft.world.level.block.SculkShriekerBlock +import net.minecraft.world.level.block.entity.BeehiveBlockEntity import net.minecraft.world.level.chunk.LevelChunk import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent 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: * - 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, ...), * 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` * (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. @@ -34,12 +38,36 @@ object OnlyPokemon { TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "removed_on_generation")) 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>() + fun onEntityJoin(event: EntityJoinLevelEvent) { 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 (mob.type.`is`(ALLOWED_MOBS)) return 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) { diff --git a/pack/config/fightorflight.json5 b/pack/config/fightorflight.json5 new file mode 100644 index 0000000..b289a64 --- /dev/null +++ b/pack/config/fightorflight.json5 @@ -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 +} \ No newline at end of file diff --git a/pack/config/regenerating_ore_veins/veins.json b/pack/config/regenerating_ore_veins/veins.json index 41e1153..c1598a7 100644 --- a/pack/config/regenerating_ore_veins/veins.json +++ b/pack/config/regenerating_ore_veins/veins.json @@ -1,6 +1,6 @@ [ { - "id": "coal_showcase", + "id": "coal_node", "blocks": [ "minecraft:coal_ore" ], @@ -14,12 +14,12 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, - "attempts": 2, + "min_radius": 0, + "max_radius": 1, + "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 4, + "chunk_minimum_generation_separation": 64, "fill_factor": 0.8, "regeneration_interval_seconds": 1200, "regeneration_interval_jitter": { @@ -66,12 +66,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 4, "attempts": 2, "min_y": 0, "max_y": 100, - "chunk_minimum_generation_separation": 4, + "chunk_minimum_generation_separation": 6, "fill_factor": 0.7, "regeneration_interval_seconds": 1200, "regeneration_interval_jitter": { @@ -118,12 +118,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 4, "attempts": 2, "min_y": -32, "max_y": -1, - "chunk_minimum_generation_separation": 4, + "chunk_minimum_generation_separation": 6, "fill_factor": 0.7, "regeneration_interval_seconds": 1200, "regeneration_interval_jitter": { @@ -132,7 +132,7 @@ } }, { - "id": "copper_showcase", + "id": "copper_node", "blocks": [ "minecraft:copper_ore" ], @@ -146,12 +146,12 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, - "attempts": 2, + "min_radius": 0, + "max_radius": 1, + "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 64, "fill_factor": 0.8, "regeneration_interval_seconds": 1800, "regeneration_interval_jitter": { @@ -299,12 +299,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 4, "attempts": 2, "min_y": 0, "max_y": 100, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 8, "fill_factor": 0.7, "regeneration_interval_seconds": 1800, "regeneration_interval_jitter": { @@ -452,12 +452,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 4, "attempts": 2, "min_y": -32, "max_y": -1, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 8, "fill_factor": 0.7, "regeneration_interval_seconds": 1800, "regeneration_interval_jitter": { @@ -466,7 +466,7 @@ } }, { - "id": "iron_showcase", + "id": "iron_node", "blocks": [ "minecraft:iron_ore" ], @@ -480,12 +480,12 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, - "attempts": 2, + "min_radius": 0, + "max_radius": 1, + "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 64, "fill_factor": 0.8, "regeneration_interval_seconds": 2400, "regeneration_interval_jitter": { @@ -567,12 +567,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 4, "attempts": 2, "min_y": 0, "max_y": 90, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 8, "fill_factor": 0.7, "regeneration_interval_seconds": 2400, "regeneration_interval_jitter": { @@ -654,12 +654,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 4, "attempts": 2, "min_y": -48, "max_y": -1, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 8, "fill_factor": 0.7, "regeneration_interval_seconds": 2400, "regeneration_interval_jitter": { @@ -668,7 +668,7 @@ } }, { - "id": "redstone_showcase", + "id": "redstone_node", "blocks": [ "minecraft:redstone_ore" ], @@ -682,12 +682,12 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, + "min_radius": 0, + "max_radius": 1, "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 64, "fill_factor": 0.8, "regeneration_interval_seconds": 2700, "regeneration_interval_jitter": { @@ -786,12 +786,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 3, "attempts": 1, "min_y": 0, "max_y": 60, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 2700, "regeneration_interval_jitter": { @@ -890,12 +890,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 3, "attempts": 1, "min_y": -56, "max_y": -1, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 2700, "regeneration_interval_jitter": { @@ -904,7 +904,7 @@ } }, { - "id": "lapis_showcase", + "id": "lapis_node", "blocks": [ "minecraft:lapis_ore" ], @@ -918,12 +918,12 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, + "min_radius": 0, + "max_radius": 1, "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 64, "fill_factor": 0.8, "regeneration_interval_seconds": 2700, "regeneration_interval_jitter": { @@ -1108,12 +1108,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 3, "attempts": 1, "min_y": 0, "max_y": 80, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 2700, "regeneration_interval_jitter": { @@ -1298,12 +1298,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 3, "attempts": 1, "min_y": -56, "max_y": -1, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 2700, "regeneration_interval_jitter": { @@ -1312,7 +1312,7 @@ } }, { - "id": "gold_showcase", + "id": "gold_node", "blocks": [ "minecraft:gold_ore" ], @@ -1326,12 +1326,12 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, - "attempts": 2, + "min_radius": 0, + "max_radius": 1, + "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 64, "fill_factor": 0.8, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -1408,12 +1408,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 3, "attempts": 2, "min_y": 0, "max_y": 120, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 7, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -1490,12 +1490,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 1, - "max_radius": 2, + "min_radius": 2, + "max_radius": 3, "attempts": 2, "min_y": -48, "max_y": -1, - "chunk_minimum_generation_separation": 5, + "chunk_minimum_generation_separation": 7, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -1504,7 +1504,7 @@ } }, { - "id": "emerald_showcase", + "id": "emerald_node", "blocks": [ "minecraft:emerald_ore" ], @@ -1518,12 +1518,12 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, - "attempts": 2, + "min_radius": 0, + "max_radius": 1, + "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 64, "fill_factor": 0.8, "regeneration_interval_seconds": 4500, "regeneration_interval_jitter": { @@ -1644,11 +1644,11 @@ ], "shape": "sphere", "min_radius": 1, - "max_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": 0, "max_y": 320, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 8, "fill_factor": 0.7, "regeneration_interval_seconds": 4500, "regeneration_interval_jitter": { @@ -1769,11 +1769,11 @@ ], "shape": "sphere", "min_radius": 1, - "max_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": -16, "max_y": -1, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 8, "fill_factor": 0.7, "regeneration_interval_seconds": 4500, "regeneration_interval_jitter": { @@ -1782,7 +1782,7 @@ } }, { - "id": "amethyst", + "id": "amethyst_node", "blocks": [ "minecraft:amethyst_block", "minecraft:budding_amethyst" @@ -1794,17 +1794,17 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:dripstone_caves" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 1, "max_radius": 1, - "attempts": 4, + "attempts": 1, "min_y": 0, - "max_y": 40, - "chunk_minimum_generation_separation": 10, - "fill_factor": 0.7, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 0.8, "regeneration_interval_seconds": 7200, "regeneration_interval_jitter": { "range_min": -720, @@ -1812,7 +1812,7 @@ } }, { - "id": "diamond", + "id": "diamond_node", "blocks": [ "minecraft:deepslate_diamond_ore" ], @@ -1822,17 +1822,17 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:deep_dark" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 1, "max_radius": 1, - "attempts": 3, - "min_y": -64, - "max_y": -24, - "chunk_minimum_generation_separation": 10, - "fill_factor": 0.7, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 0.8, "regeneration_interval_seconds": 5400, "regeneration_interval_jitter": { "range_min": -540, @@ -1840,7 +1840,7 @@ } }, { - "id": "quartz", + "id": "quartz_node", "blocks": [ "minecraft:nether_quartz_ore" ], @@ -1850,17 +1850,17 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:basalt_deltas" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 1, - "max_radius": 2, - "attempts": 3, + "max_radius": 1, + "attempts": 1, "min_y": 0, - "max_y": 40, - "chunk_minimum_generation_separation": 8, - "fill_factor": 0.7, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 0.8, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { "range_min": -360, @@ -1868,7 +1868,7 @@ } }, { - "id": "netherite", + "id": "netherite_node", "blocks": [ "minecraft:ancient_debris" ], @@ -1878,23 +1878,51 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:basalt_deltas" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 0, - "max_radius": 1, - "attempts": 2, + "max_radius": 0, + "attempts": 1, "min_y": 0, - "max_y": 40, - "chunk_minimum_generation_separation": 14, - "fill_factor": 0.7, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, "regeneration_interval_seconds": 10800, "regeneration_interval_jitter": { "range_min": -1080, "range_max": 1080 } }, + { + "id": "thunder_stone_node", + "blocks": [ + "cobblemon:thunder_stone_ore" + ], + "weights": [ + 1 + ], + "dimension_whitelist": [ + "minecraft:overworld" + ], + "area_whitelist": [ + "selftest_nowhere" + ], + "shape": "sphere", + "min_radius": 0, + "max_radius": 0, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, + "regeneration_interval_seconds": 3600, + "regeneration_interval_jitter": { + "range_min": -360, + "range_max": 360 + } + }, { "id": "thunder_stone", "blocks": [ @@ -1969,12 +1997,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": 0, "max_y": 100, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2056,12 +2084,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": -48, "max_y": -1, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2070,7 +2098,7 @@ } }, { - "id": "shiny_stone", + "id": "shiny_stone_node", "blocks": [ "cobblemon:shiny_stone_ore" ], @@ -2080,17 +2108,17 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:lush_caves" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 0, "max_radius": 1, - "attempts": 4, + "attempts": 1, "min_y": 0, - "max_y": 40, - "chunk_minimum_generation_separation": 10, - "fill_factor": 0.7, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 0.8, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { "range_min": -360, @@ -2098,9 +2126,9 @@ } }, { - "id": "shiny_stone_deep", + "id": "leaf_stone_node", "blocks": [ - "cobblemon:deepslate_shiny_stone_ore" + "cobblemon:leaf_stone_ore" ], "weights": [ 1 @@ -2108,17 +2136,17 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:lush_caves" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 0, - "max_radius": 1, - "attempts": 4, - "min_y": -16, - "max_y": -1, - "chunk_minimum_generation_separation": 10, - "fill_factor": 0.7, + "max_radius": 0, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { "range_min": -360, @@ -2216,12 +2244,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": 0, "max_y": 100, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2320,12 +2348,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": -48, "max_y": -1, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2334,7 +2362,7 @@ } }, { - "id": "water_stone_showcase", + "id": "water_stone_node", "blocks": [ "cobblemon:water_stone_ore" ], @@ -2348,13 +2376,13 @@ "selftest_nowhere" ], "shape": "sphere", - "min_radius": 2, - "max_radius": 2, + "min_radius": 0, + "max_radius": 0, "attempts": 1, "min_y": 0, "max_y": 0, - "chunk_minimum_generation_separation": 8, - "fill_factor": 0.8, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { "range_min": -360, @@ -2436,12 +2464,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": 0, "max_y": 100, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2524,12 +2552,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": -48, "max_y": -1, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2538,7 +2566,7 @@ } }, { - "id": "moon_stone", + "id": "moon_stone_node", "blocks": [ "cobblemon:dripstone_moon_stone_ore", "cobblemon:moon_stone_ore", @@ -2552,17 +2580,45 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:dripstone_caves" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 0, "max_radius": 1, - "attempts": 4, + "attempts": 1, "min_y": 0, - "max_y": 40, - "chunk_minimum_generation_separation": 10, - "fill_factor": 0.7, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 0.8, + "regeneration_interval_seconds": 3600, + "regeneration_interval_jitter": { + "range_min": -360, + "range_max": 360 + } + }, + { + "id": "sun_stone_node", + "blocks": [ + "cobblemon:sun_stone_ore" + ], + "weights": [ + 1 + ], + "dimension_whitelist": [ + "minecraft:overworld" + ], + "area_whitelist": [ + "selftest_nowhere" + ], + "shape": "sphere", + "min_radius": 0, + "max_radius": 0, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { "range_min": -360, @@ -2746,12 +2802,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": 0, "max_y": 100, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2936,12 +2992,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 1, "min_y": -48, "max_y": -1, - "chunk_minimum_generation_separation": 8, + "chunk_minimum_generation_separation": 11, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -2949,6 +3005,34 @@ "range_max": 360 } }, + { + "id": "fire_stone_node", + "blocks": [ + "cobblemon:fire_stone_ore" + ], + "weights": [ + 1 + ], + "dimension_whitelist": [ + "minecraft:overworld" + ], + "area_whitelist": [ + "selftest_nowhere" + ], + "shape": "sphere", + "min_radius": 0, + "max_radius": 0, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, + "regeneration_interval_seconds": 3600, + "regeneration_interval_jitter": { + "range_min": -360, + "range_max": 360 + } + }, { "id": "fire_stone", "blocks": [ @@ -3018,12 +3102,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": 0, "max_y": 120, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -3100,12 +3184,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": -48, "max_y": -1, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -3113,6 +3197,34 @@ "range_max": 360 } }, + { + "id": "ice_stone_node", + "blocks": [ + "cobblemon:ice_stone_ore" + ], + "weights": [ + 1 + ], + "dimension_whitelist": [ + "minecraft:overworld" + ], + "area_whitelist": [ + "selftest_nowhere" + ], + "shape": "sphere", + "min_radius": 0, + "max_radius": 0, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, + "regeneration_interval_seconds": 3600, + "regeneration_interval_jitter": { + "range_min": -360, + "range_max": 360 + } + }, { "id": "ice_stone", "blocks": [ @@ -3225,12 +3337,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": 0, "max_y": 320, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -3350,12 +3462,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": -16, "max_y": -1, - "chunk_minimum_generation_separation": 6, + "chunk_minimum_generation_separation": 9, "fill_factor": 0.7, "regeneration_interval_seconds": 3600, "regeneration_interval_jitter": { @@ -3363,6 +3475,34 @@ "range_max": 360 } }, + { + "id": "dawn_stone_node", + "blocks": [ + "cobblemon:dawn_stone_ore" + ], + "weights": [ + 1 + ], + "dimension_whitelist": [ + "minecraft:overworld" + ], + "area_whitelist": [ + "selftest_nowhere" + ], + "shape": "sphere", + "min_radius": 0, + "max_radius": 0, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 1.0, + "regeneration_interval_seconds": 4500, + "regeneration_interval_jitter": { + "range_min": -450, + "range_max": 450 + } + }, { "id": "dawn_stone", "blocks": [ @@ -3475,12 +3615,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": 0, "max_y": 320, - "chunk_minimum_generation_separation": 7, + "chunk_minimum_generation_separation": 10, "fill_factor": 0.7, "regeneration_interval_seconds": 4500, "regeneration_interval_jitter": { @@ -3600,12 +3740,12 @@ "minecraft:lush_caves" ], "shape": "sphere", - "min_radius": 0, - "max_radius": 1, + "min_radius": 1, + "max_radius": 2, "attempts": 2, "min_y": -16, "max_y": -1, - "chunk_minimum_generation_separation": 7, + "chunk_minimum_generation_separation": 10, "fill_factor": 0.7, "regeneration_interval_seconds": 4500, "regeneration_interval_jitter": { @@ -3614,7 +3754,7 @@ } }, { - "id": "dusk_stone", + "id": "dusk_stone_node", "blocks": [ "cobblemon:deepslate_dusk_stone_ore" ], @@ -3624,17 +3764,17 @@ "dimension_whitelist": [ "minecraft:overworld" ], - "biome_whitelist": [ - "minecraft:deep_dark" + "area_whitelist": [ + "selftest_nowhere" ], "shape": "sphere", "min_radius": 0, "max_radius": 1, - "attempts": 3, - "min_y": -64, - "max_y": -24, - "chunk_minimum_generation_separation": 12, - "fill_factor": 0.7, + "attempts": 1, + "min_y": 0, + "max_y": 0, + "chunk_minimum_generation_separation": 64, + "fill_factor": 0.8, "regeneration_interval_seconds": 4500, "regeneration_interval_jitter": { "range_min": -450, diff --git a/pack/index.toml b/pack/index.toml index 5d09fe2..4f36510 100644 --- a/pack/index.toml +++ b/pack/index.toml @@ -24,6 +24,10 @@ hash = "84f1468a01fcd0f0f728446c386cbb0d4b3df0a14883fa6da9f4f70e4519ba80" file = "config/defaultoptions/keybindings.txt" hash = "fbc8a858d5cfe978ed48a6a5f564b9d0a7b540e12b5f481c4eb797021a9f6078" +[[files]] +file = "config/fightorflight.json5" +hash = "d1f8daadc8e34c41950e8f439ca089e8820142e57767459cbe1eb2a250ee4441" + [[files]] file = "config/lootr-common.toml" hash = "ca348d022ad97e74ec51a896060e8fcfc079cf0d34ef116a967e72f60bdb3381" @@ -50,7 +54,7 @@ hash = "a6e9d717e2f73e463b25b1eacfbff43304fd8993937d8c002f9629f24ebcc221" [[files]] file = "config/regenerating_ore_veins/veins.json" -hash = "ead417ded7b2867aadf6946c5f919b95fb2001d5207c1c39753872a2676a5d79" +hash = "d68a080ec5b881d6af1c5f06fbc2ca0d275b8898fc251f96ad3c1471394a8bd3" [[files]] file = "config/worldgen_editor/presets/cobblesurvival_island.json" diff --git a/pack/pack.toml b/pack/pack.toml index fe4d2e8..c920443 100644 --- a/pack/pack.toml +++ b/pack/pack.toml @@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0" [index] file = "index.toml" hash-format = "sha256" -hash = "f13be586ab9aaa36fb1db267f406efd31c227e77f20ed6e971306c164e15757f" +hash = "90997231a86493a8e7488c2c295b314d3bfcfcfc670fb3f7699e827054b5823c" [versions] minecraft = "1.21.1" diff --git a/tools/biome_layout.py b/tools/biome_layout.py index 9f4f3bb..c392667 100644 --- a/tools/biome_layout.py +++ b/tools/biome_layout.py @@ -4,8 +4,11 @@ Vanilla's overworld biome parameters come from Minecraft's own data generator (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 -from world/biomes.json, the extra entries are appended, and the result becomes -the inline biome list of WorldGen Editor's delegate in +from world/biomes.json ("remap"), or cut into pieces along one climate +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. The world type has no Nether dimension. @@ -38,11 +41,28 @@ def vanilla_parameters(): 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(): spec = json.loads((ROOT / "world/biomes.json").read_text()) targets = set(spec["allowed"]) | set(spec["water"]) entries, unmapped = [], set() + splits = {s["biome"]: s for s in spec.get("split", [])} for entry in vanilla_parameters(): + if entry["biome"] in splits: + entries += split(entry, splits[entry["biome"]]) + continue target = spec["remap"].get(entry["biome"]) if target is None: unmapped.add(entry["biome"]) diff --git a/tools/make_world.py b/tools/make_world.py index 8e8f7f7..ab18d46 100644 --- a/tools/make_world.py +++ b/tools/make_world.py @@ -8,12 +8,11 @@ 0,0 (the island plus a strip of sea; the sea corners inside the square border generate when someone gets there). 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 - it is loaded, 8 a tick, and Chunky is faster), place the showcase ore veins (world/zones.json → - "ores", tiers with "showcase": one vein sticking out of the ground near - its zone's sample point, so players see what a regenerating vein looks - like), save, measure where the land ends (tools/world_report.py) and set - a square world border `border_margin` blocks outside the outermost coast. + Ore Veins has put its veins and the companion mod its ore nodes into every + chunk (both only fill a new chunk while it is loaded, a few a tick, and + Chunky is faster), save, measure where the land ends (tools/world_report.py) + and set a square world border `border_margin` blocks outside the outermost + coast. 4. Copy the world to build/world-template without player data, then run 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)) from mcserver import BUILD, ROOT, SERVER, Server, chunky, eula_accepted, prepare, scan_log # noqa: E402 import world_report # noqa: E402 -import zone_grid # noqa: E402 from numpy import hypot as np_hypot # noqa: E402 TEMPLATE = BUILD / "world-template" 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): @@ -72,45 +86,6 @@ def fill_veins(server, radius): 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(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.parse_args() @@ -133,7 +108,6 @@ def main(): seconds = server.pregenerate(0, 0, recipe["pregen_radius"], shape="circle") print(f"pregeneration finished in {seconds:.0f}s") fill_veins(server, recipe["pregen_radius"]) - place_showcases(server) server.command("save-all flush") cx, cz, size = border_for(world, recipe) print(f" {server.command(f'worldborder center {cx} {cz}').strip()}") diff --git a/tools/world_report.py b/tools/world_report.py index c6bf717..b5b40b4 100644 --- a/tools/world_report.py +++ b/tools/world_report.py @@ -3,15 +3,17 @@ * 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 - a regenerating vein (Regenerating Ore Veins' saved data), and each vein's - blocks must lie in the zone of its ore tier (world/zones.json → "ores"). + a regenerating vein (Regenerating Ore Veins' saved data: the underground + 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 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 (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 - the check, and so (except with --sample) do an ore tier without any vein - and a chunk the mod never put veins into. + the check, and so (except with --sample) do an ore tier without nodes, a + 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) must be listed in world/biomes.json ("allowed" or "water"). * End access: at least one stronghold must start inside the world border. @@ -231,20 +233,37 @@ def read_veins(world): 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): """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()) grid = zone_grid.load() cave_biome = {c["id"]: c["biome"] for c in plan["caves"]} 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: vein = veins.get((x, y, z)) if vein is None: if ORE.search(name): # amethyst outside a vein is a natural geode untracked[name] = untracked.get(name, 0) + 1 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" counts = per_tier.setdefault(tier, {}) 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)) problems = [f"{n} {name} outside any regenerating vein" for name, n in sorted(untracked.items())] 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] - unfilled = [c for c in full_chunks if c not in filled] + problems += [f"ore tier {tier} has no ore nodes in the world" for tier in plan["ores"] if (tier, "node") not in kinds] + 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: - 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]})" for (vein, zone, want), at in sorted(stray.items(), key=str)] return per_tier, problems diff --git a/tools/zone_ores.py b/tools/zone_ores.py index baef1f1..e6d0d58 100755 --- a/tools/zone_ores.py +++ b/tools/zone_ores.py @@ -1,25 +1,28 @@ #!/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, 24-block cells; rows of cells merged into rectangles), named _, reaching from bedrock to 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. - * veins.json: one vein per tier, or two when the tier has "stone" and "deepslate" blocks (an upper vein - from y 0 up, "_deep" below). A surface tier's veins are limited to its zone's boxes and kept out - of the cave biomes, which are zones of their own; a cave tier's veins to its cave biome. The mod checks - the area for every block and the biome at a vein's centre. One vein only for the self-tests comes - last ("selftest", a single coal block); its area lies far outside the world border, so it never - generates, and the tests place it by command. Each tier with "showcase" also gets "_showcase", a full - radius-2 vein in the same place-only area, which tools/make_world.py places by hand near the zone's sample point. + * veins.json: a surface tier's underground veins ("veins"): one vein, or two when the tier has "stone" and + "deepslate" blocks (an upper vein from y 0 up, "_deep" below), limited to its zone's boxes and kept + out of the cave biomes, which are zones of their own. The mod checks the area for every block and the + biome at a vein's centre. Then every tier's "_node": the ore node (the stone ore, or the tier's + blocks), placed only by the companion mod, so its area lies far outside the world border, where the mod + never generates it by itself. The same goes for "selftest" (a single coal block the self-tests place). * 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_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 -reads it by default): what each tier's blocks drop and in which zone. +It also writes datapacks/core/data/cobblesurvival/ore_nodes/island.json, the node table the companion mod reads +(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. @@ -35,6 +38,7 @@ LAYOUT = ROOT / "build/zone-layout.json" BOTTOM, TOP = -64, 320 SELFTEST_AREA = "selftest_nowhere" 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 "_ore" → "". DROPS = {"iron": "raw_iron", "copper": "raw_copper", "gold": "raw_gold", "lapis": "lapis_lazuli", "nether_quartz": "quartz", "amethyst_block": "amethyst_block", "budding_amethyst": "amethyst_shard"} @@ -83,7 +87,8 @@ def zone_areas(layout, zone_id): 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 jitter = round(seconds * spec.get("jitter", defaults["jitter"])) return { @@ -93,13 +98,13 @@ def vein(vein_id, blocks, spec, defaults, y_range, where): "dimension_whitelist": ["minecraft:overworld"], **where, "shape": "sphere", - "min_radius": spec["radius"][0], - "max_radius": spec["radius"][1], - "attempts": spec.get("attempts", defaults["attempts"]), + "min_radius": shape["radius"][0], + "max_radius": shape["radius"][1], + "attempts": shape.get("attempts", defaults["attempts"]), "min_y": y_range[0], "max_y": y_range[1], - "chunk_minimum_generation_separation": spec["spacing"], - "fill_factor": spec.get("fill", defaults["fill"]), + "chunk_minimum_generation_separation": shape.get("spacing", 64), + "fill_factor": shape.get("fill", defaults["fill"]), "regeneration_interval_seconds": seconds, "regeneration_interval_jitter": {"range_min": -jitter, "range_max": jitter}, } @@ -122,34 +127,41 @@ def main(): caves = {c["id"]: c["biome"] for c in plan["caves"]} 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(): zone = spec["zone"] - if zone in caves: - where = {"biome_whitelist": [caves[zone]]} - elif zone in surface: - if zone not in area_names: - boxes = zone_areas(layout, zone) - if not boxes: - sys.exit(f"zone {zone} (ore tier {tier}) has no cells in the grid") - areas += boxes - area_names[zone] = [a["name"] for a in boxes] - where = {"area_whitelist": area_names[zone], "biome_blacklist": sorted(caves.values())} - else: + if zone not in caves and zone not in surface: sys.exit(f"ore tier {tier}: unknown zone {zone}") - 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}) + # 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: + node.update({"biome": caves[zone], "tries": spec["nodes"]["tries"], "y": spec["nodes"]["y"]}) + nodes.append(node) + if "veins" not in spec: + continue + if zone not in area_names: + boxes = zone_areas(layout, zone) + if not boxes: + sys.exit(f"zone {zone} (ore tier {tier}) has no cells in the grid") + areas += boxes + area_names[zone] = [a["name"] for a in boxes] + where = {"area_whitelist": area_names[zone], "biome_blacklist": sorted(caves.values())} + shape = spec["veins"] + low, high = shape["y"] 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 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: 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", "x": 29_000_000, "y": 0, "z": 29_000_000, "dimx": 0, "dimy": 0, "dimz": 0}) @@ -177,12 +189,18 @@ def main(): "durability": 2, "stack_size": 64, }, 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"]} sources = [] 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)] + 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): - 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": []}) SOURCES.write_text(json.dumps({ "_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 = {} for tier, spec in plan["ores"].items(): 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(): 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__": diff --git a/world/biomes.json b/world/biomes.json index 410aec0..1df4d39 100644 --- a/world/biomes.json +++ b/world/biomes.json @@ -79,6 +79,17 @@ "minecraft:dripstone_caves": "minecraft:dripstone_caves", "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": [ { "_comment": "Volcanic caves under the hottest, driest land (where desert and badlands are), at cave depth like lush and dripstone caves.", diff --git a/world/vein-sources.json b/world/vein-sources.json index 35f8160..c4550ad 100644 --- a/world/vein-sources.json +++ b/world/vein-sources.json @@ -3,133 +3,133 @@ "sources": [ { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] }, { "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", "needs": [] } diff --git a/world/zones.json b/world/zones.json index a6eb306..29d9845 100644 --- a/world/zones.json +++ b/world/zones.json @@ -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 '_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, "inherit": ["minecraft:river"], "guardian_respawn_hours": 5, "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": { - "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}, - "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}, - "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}, - "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}, - "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}, - "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}, - "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}, - "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}, - "diamond": {"zone": "deep_dark", "deepslate": "minecraft:deepslate_diamond_ore", "radius": [1, 1], "y": [-64, -24], "spacing": 10, "regen_minutes": 90, "attempts": 3}, - "quartz": {"zone": "basalt", "blocks": [["minecraft:nether_quartz_ore", 1]], "radius": [1, 2], "y": [0, 40], "spacing": 8, "regen_minutes": 60, "attempts": 3}, - "netherite": {"zone": "basalt", "blocks": [["minecraft:ancient_debris", 1]], "radius": [0, 1], "y": [0, 40], "spacing": 14, "regen_minutes": 180, "attempts": 2}, - "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}, - "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}, - "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}, - "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}, - "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}, - "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}, - "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}, - "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}, - "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}, - "dusk_stone": {"zone": "deep_dark", "deepslate": "cobblemon:deepslate_dusk_stone_ore", "radius": [0, 1], "y": [-64, -24], "spacing": 12, "regen_minutes": 75, "attempts": 3} + "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", "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", "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", "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", "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", "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", "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]], "regen_minutes": 120, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [0, 40]}}, + "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]], "regen_minutes": 60, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [0, 40]}}, + "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", "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", "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", "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", "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]], "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", "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", "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", "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", "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", "regen_minutes": 75, "nodes": {"chance": 0.1, "tries": 8, "radius": [0, 1], "y": [-64, -24]}} }, "species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt"}, "zones": [ @@ -72,9 +72,9 @@ } ], "caves": [ - {"id": "lush_caves", "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": "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": "basalt", "lines": 15, "name": "Basalt Furnace", "biome": "minecraft:basalt_deltas", "levels": [55, 70], "hazard": "heat (Cold Sweat)"} + {"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", "types": ["rock", "ground", "steel", "fairy", "psychic", "dragon"], "lines": 20, "name": "Dripstone Depths", "biome": "minecraft:dripstone_caves", "levels": [22, 38], "hazard": "none"}, + {"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", "types": ["fire", "rock", "ground", "steel", "dragon", "dark"], "lines": 15, "name": "Basalt Furnace", "biome": "minecraft:basalt_deltas", "levels": [55, 70], "hazard": "heat (Cold Sweat)"} ] }