From 173203eeb58538134af8df88f3492d5ec617f57d Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 30 Oct 2026 01:50:56 +0100 Subject: [PATCH] Phase 3: soft level pressure, less experience far above a zone's band - Companion mod (zone/LevelPressure.kt): a Pokemon more than 5 levels above the band of the zone its trainer is in gets 10% less experience per extra level, down to 10%. Exp. Candy and commands are exempt; no hard level caps (roadmap). - Rules in world/zones.json -> level_pressure, shipped in the zone layout. - Self-test level_pressure_cuts_xp. Co-Authored-By: Claude Opus 5.5 --- .../data/cobblesurvival/zones/island.json | 2 +- docs/balance.md | 1 + docs/checklists/phase-3.md | 5 ++- docs/design.md | 2 ++ .../dev/cobblesurvival/CobbleSurvival.kt | 2 ++ .../dev/cobblesurvival/selftest/ZoneTests.kt | 31 ++++++++++++++++ .../dev/cobblesurvival/zone/LevelPressure.kt | 35 +++++++++++++++++++ .../kotlin/dev/cobblesurvival/zone/Zones.kt | 12 +++++++ tools/zone_layout.py | 1 + world/zones.json | 3 +- 10 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 mod/src/main/kotlin/dev/cobblesurvival/zone/LevelPressure.kt diff --git a/datapacks/core/data/cobblesurvival/zones/island.json b/datapacks/core/data/cobblesurvival/zones/island.json index 86564b4..4a554cf 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"}],"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":[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]]} diff --git a/docs/balance.md b/docs/balance.md index 9a1782d..1308228 100644 --- a/docs/balance.md +++ b/docs/balance.md @@ -27,6 +27,7 @@ Every tunable lives in a config file or in this table. **Placeholder** means gue | Guardian strength | strongest Pokémon spawning in the zone, band top + 5 levels, 1.5× size, uncatchable | Placeholder | `tools/zone_spawns.py` (`GUARDIAN_*`), `world/zones.json` → `guardian.species` | | | Guardian drops | Button Isle 10 Poké Balls + 4 Potions; Southwold 6 Great Balls + 4 Exp. Candy S; Westmarch and Jungle Mire Waders + 4 Pecha Berries + 2 Exp. Candy M; Mire and Highlands 3 Icy Rocks + 4 Super Potions; Sunscorch 3 Heat Rocks + 2 Exp. Candy L; Frostpeak and Deep Dark 3 Icy Rocks + 2 Exp. Candy L; Deep Sea Mystic Water + 3 Exp. Candy M | Placeholder | `world/zones.json` → `guardian.drops` | 3 rocks make one full fur or hide set | | Mire poison | Poison I for 7 s, renewed every 5 s while in the Mire without Mire Waders | Placeholder | `datapacks/core/data/cobblesurvival/zone_hazards/mire.json` | Vanilla Poison stops at half a heart | +| Level pressure | full experience up to 5 levels above the zone's band, then 10 % less per level, at least 10 % (Exp. Candy and commands exempt) | Placeholder | `world/zones.json` → `level_pressure` | E.g. on Button Isle (up to Lv 10): Lv 15 full, Lv 20 half, Lv 24+ a tenth | | 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 | | Gamerules in the template | `doMobSpawning false` | Decided (Adrian, 2026-10-28; may be reverted later) | `world/world.json` → `gamerules` | Pokémon spawning is Cobblemon's own `doPokemonSpawning` | diff --git a/docs/checklists/phase-3.md b/docs/checklists/phase-3.md index c2b97f0..2b817b6 100644 --- a/docs/checklists/phase-3.md +++ b/docs/checklists/phase-3.md @@ -21,7 +21,9 @@ Open `build/zone-map.png` (the island coloured by zone) and read the draft in `d - **The Mire's poison** (step 5). Mire Waders from the Westmarch and Jungle guardians keep it off. Automated: `mire_poisons_without_waders`. -Still to come: soft level pressure, flyers. +- **Soft level pressure** (step 6). Pokémon far above a zone's band learn less there. Automated: `level_pressure_cuts_xp`. + +Still to come: flyers. ## In-game @@ -33,3 +35,4 @@ Still to come: soft level pressure, flyers. - [ ] **Lowlands:** a night in Southwold or on Button Isle without gear never gets dangerous. - [ ] **Guardians:** `/cobblesurvival guardians` (operator) lists each zone's guardian and spot. Walk there: a big Pokémon appears, can't be caught, and beating it puts its drops in your inventory with a gold message. It's gone for 5 hours of play. - [ ] **Mire:** walk into the Mangrove Mire: a green warning, then Poison keeps coming back. With Mire Waders on (from the Westmarch or Jungle guardian) nothing happens. +- [ ] **Level pressure:** bring a Lv 30 Pokémon to Button Isle and win a battle: it gains only a sliver of experience, while a Lv 10 one gains the full amount. diff --git a/docs/design.md b/docs/design.md index 9a5aec6..4661a4e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -111,6 +111,8 @@ Order of play the bands suggest: Button Isle → Southwold → Westmarch or Jung **The Mire's poison** (Phase 3, step 5): while a player is in the Mangrove Mire without Mire Waders on, the companion mod gives them Poison every 5 seconds (`zone/ZoneHazards.kt`, data in `datapacks/core/data/cobblesurvival/zone_hazards/mire.json`, protection tag `#cobblesurvival:mire_protection`); the first time they walk in unprotected they get a warning. Vanilla Poison never kills, but it pins a player at half a heart, so a fall or a Pokémon's hit becomes deadly; milk, potions and golden apples still help. Mire Waders (leather boots, a KubeJS item) drop from the guardians of Westmarch and the Verdant Jungle, the zones before the Mire. Tested: `mire_poisons_without_waders`. +**Soft level pressure** (Phase 3, step 6; no hard level caps): a Pokémon more than 5 levels above the band of the zone its trainer is in gets 10 % less experience per extra level, down to 10 % (`zone/LevelPressure.kt`, values in `world/zones.json` → `level_pressure`). It counts battle experience and anything else except Exp. Candy and commands. The roadmap also offered a disobedience chance; experience alone is gentler and Cobblemon has nothing to build disobedience on. Tested: `level_pressure_cuts_xp`. + **Alpha guardians** (Phase 3, step 4; Adrian, 2026-10-29: fixed spots, back 5 hours after being beaten): - One per zone except the Shallows, Lush Caves and Dripstone; the Basalt Furnace gets its boss in Phase 7. Each stands at its zone's sample point (deep inside the zone; a cave floor for the Deep Dark) and only appears while a player is within 96 blocks; it wanders no further than 24 blocks. - The guardian is the strongest Pokémon that spawns walking (or, at sea, swimming) in its zone, five levels above the band, half again as big, and uncatchable: Pachirisu (Button Isle), Sigilyph (Southwold), Carbink (Westmarch), Tangrowth (Jungle), Arcanine (Mire), Cinderace (Highlands), Delphox (Sunscorch), Tyranitar (Frostpeak), Kingdra (Deep Sea), Dusknoir (Deep Dark). `world/zones.json` → `guardian.species` overrides the pick. diff --git a/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt b/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt index 9ccafd8..f3b540f 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt @@ -8,6 +8,7 @@ import dev.cobblesurvival.permadeath.Permadeath import dev.cobblesurvival.selftest.SelfTest import dev.cobblesurvival.world.OnlyPokemon import dev.cobblesurvival.zone.Guardians +import dev.cobblesurvival.zone.LevelPressure import dev.cobblesurvival.zone.ZoneCondition import dev.cobblesurvival.zone.ZoneHazards import dev.cobblesurvival.zone.Zones @@ -59,6 +60,7 @@ class CobbleSurvival { NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners) Guardians.register() ZoneHazards.register() + LevelPressure.register() SelfTest.register() } diff --git a/mod/src/main/kotlin/dev/cobblesurvival/selftest/ZoneTests.kt b/mod/src/main/kotlin/dev/cobblesurvival/selftest/ZoneTests.kt index 6dc6917..aaf0420 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/selftest/ZoneTests.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/selftest/ZoneTests.kt @@ -1,6 +1,8 @@ package dev.cobblesurvival.selftest import com.cobblemon.mod.common.Cobblemon +import com.cobblemon.mod.common.api.pokemon.experience.CandyExperienceSource +import com.cobblemon.mod.common.api.pokemon.experience.SidemodExperienceSource import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools import com.cobblemon.mod.common.api.spawning.SpawnCause import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail @@ -9,6 +11,7 @@ import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePosit import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput import com.cobblemon.mod.common.util.spawner import dev.cobblesurvival.CobbleSurvival +import dev.cobblesurvival.zone.LevelPressure import dev.cobblesurvival.zone.Zone import dev.cobblesurvival.zone.ZoneSample import dev.cobblesurvival.zone.Zones @@ -16,7 +19,9 @@ import net.minecraft.core.BlockPos import net.minecraft.gametest.framework.GameTestAssertException import net.minecraft.server.level.ServerPlayer import net.minecraft.util.Mth +import net.minecraft.world.item.ItemStack import net.minecraft.world.level.levelgen.Heightmap +import kotlin.math.roundToInt /** * Roadmap Phase 3: each zone has its own Pokémon and levels. The spawn pools come from tools/zone_spawns.py; @@ -30,10 +35,36 @@ object ZoneTests { fun all() = listOf( SelfTestCase("spawn_pools_are_zoned", 20, ::spawnPoolsAreZoned), SelfTestCase("spawn_survey", 2400, ::spawnSurvey, islandOnly = true), + SelfTestCase("level_pressure_cuts_xp", 100, ::levelPressure), ) private fun zoneOf(detail: SpawnDetail): Zone? = Zones.byId[detail.id.substringBefore('-')] + /** On Button Isle (band up to 10, 5 levels' grace): a Lv 10 Pokémon keeps its experience, a Lv 25 one 10 %. */ + private fun levelPressure(s: Scenario) { + val zone = Zones.byId["button_isle"] ?: throw GameTestAssertException("no Button Isle zone") + val sample = Zones.samples["button_isle"] ?: throw GameTestAssertException("no sample point for Button Isle") + val player = s.player() + val inBand = s.give(player, s.pokemon("bidoof", zone.maxLevel)) + val farAbove = s.give(player, s.pokemon("bidoof", zone.maxLevel + 15)) + val expected = (1000 * LevelPressure.factor(farAbove.level, zone)).roundToInt() + s.helper.startSequence() + .thenExecute { + s.level.getChunk(sample.x shr 4, sample.z shr 4) + s.moveTo(player, sample.x + 0.5, s.level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z).toDouble(), sample.z + 0.5) + } + .thenExecute { + val battle = SidemodExperienceSource(CobbleSurvival.MOD_ID) + val kept = inBand.addExperienceWithPlayer(player, battle, 1000).experienceAdded + val cut = farAbove.addExperienceWithPlayer(player, battle, 1000).experienceAdded + val candy = farAbove.addExperienceWithPlayer(player, CandyExperienceSource(player, ItemStack.EMPTY), 1000).experienceAdded + s.check(kept == 1000) { "a Pokémon at the band's top got $kept of 1000 experience" } + s.check(cut == expected) { "a Pokémon 15 levels above the band got $cut of 1000 experience, not $expected" } + s.check(candy == 1000) { "Exp. Candy was cut too: $candy of 1000" } + } + .thenSucceed() + } + /** Every loaded spawn entry belongs to a zone and spawns within its level band; none of Cobblemon's is left. */ private fun spawnPoolsAreZoned(s: Scenario) { val details = CobblemonSpawnPools.WORLD_SPAWN_POOL.details diff --git a/mod/src/main/kotlin/dev/cobblesurvival/zone/LevelPressure.kt b/mod/src/main/kotlin/dev/cobblesurvival/zone/LevelPressure.kt new file mode 100644 index 0000000..ae59b75 --- /dev/null +++ b/mod/src/main/kotlin/dev/cobblesurvival/zone/LevelPressure.kt @@ -0,0 +1,35 @@ +package dev.cobblesurvival.zone + +import com.cobblemon.mod.common.api.Priority +import com.cobblemon.mod.common.api.events.CobblemonEvents +import com.cobblemon.mod.common.api.events.pokemon.ExperienceGainedEvent +import com.cobblemon.mod.common.api.pokemon.experience.CandyExperienceSource +import com.cobblemon.mod.common.api.pokemon.experience.CommandExperienceSource +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Soft level pressure (roadmap Phase 3; no hard level caps): a Pokémon far above the level band of the zone its + * trainer is in learns less from battles there. Up to [LevelPressureRules.graceLevels] above the band's top it + * gets full experience; every level beyond costs [LevelPressureRules.perLevel] of it, down to + * [LevelPressureRules.minimum]. Exp. Candy and commands are never cut. + */ +object LevelPressure { + fun register() { + CobblemonEvents.EXPERIENCE_GAINED_EVENT_PRE.subscribe(Priority.NORMAL, ::onExperience) + } + + /** The share of experience a Pokémon of [level] keeps in [zone]. */ + fun factor(level: Int, zone: Zone, rules: LevelPressureRules = Zones.levelPressure): Double { + val over = level - (zone.maxLevel + rules.graceLevels) + return if (over <= 0) 1.0 else max(rules.minimum, 1.0 - over * rules.perLevel) + } + + private fun onExperience(event: ExperienceGainedEvent.Pre) { + if (event.source is CandyExperienceSource || event.source is CommandExperienceSource) return + val player = event.pokemon.getOwnerPlayer() ?: return + val zone = Zones.at(player.serverLevel(), player.blockPosition()) ?: return + val keep = factor(event.pokemon.level, zone) + if (keep < 1.0) event.experience = max(1, (event.experience * keep).roundToInt()) + } +} diff --git a/mod/src/main/kotlin/dev/cobblesurvival/zone/Zones.kt b/mod/src/main/kotlin/dev/cobblesurvival/zone/Zones.kt index b0eb199..b5357b4 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/zone/Zones.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/zone/Zones.kt @@ -19,6 +19,9 @@ import net.neoforged.neoforge.event.AddReloadListenerEvent /** One zone of the island (world/zones.json). */ data class Zone(val id: String, val name: String, val minLevel: Int, val maxLevel: Int) +/** Soft level pressure (world/zones.json → level_pressure). */ +data class LevelPressureRules(val graceLevels: Int = 5, val perLevel: Double = 0.1, val minimum: Double = 0.1) + /** A point deep inside a zone (y only for cave zones), used by the spawn survey. */ data class ZoneSample(val x: Int, val y: Int?, val z: Int) @@ -36,6 +39,8 @@ object Zones : SimpleJsonResourceReloadListener(Gson(), "zones") { private var caves: Map = emptyMap() var samples: Map = emptyMap() private set + var levelPressure = LevelPressureRules() + private set /** For zones made of biomes: a point inside their own hazard biome (Frostpeak's jagged peaks, ...). */ var biomeSamples: Map = emptyMap() private set @@ -58,6 +63,13 @@ object Zones : SimpleJsonResourceReloadListener(Gson(), "zones") { val caveZones = json.getAsJsonArray("caves").map { it.asJsonObject } caves = caveZones.associate { ResourceLocation.parse(it.get("biome").asString) to zone(it) } byId = (byIndex + caves.values).associateBy { it.id } + levelPressure = json.getAsJsonObject("level_pressure")?.let { rules -> + LevelPressureRules( + graceLevels = rules.get("grace_levels")?.asInt ?: 5, + perLevel = rules.get("per_level")?.asDouble ?: 0.1, + minimum = rules.get("minimum")?.asDouble ?: 0.1, + ) + } ?: LevelPressureRules() samples = samplesOf(json, "samples") biomeSamples = samplesOf(json, "biome_samples") radius = json.get("radius").asInt diff --git a/tools/zone_layout.py b/tools/zone_layout.py index 9950c30..324f19c 100755 --- a/tools/zone_layout.py +++ b/tools/zone_layout.py @@ -312,6 +312,7 @@ def write_datapack(zone, plan, preview): "seed": preview["seed"], "radius": preview["radius"], "step": preview["step"], "size": preview["size"], "zones": [{k: z[k] for k in keep} for z in plan["zones"]], "caves": [{**{k: c[k] for k in keep}, "biome": c["biome"]} for c in plan["caves"]], + "level_pressure": plan.get("level_pressure", {}), "samples": samples(zone, plan, preview), "biome_samples": biome_samples(zone, plan, preview), "grid_rle": runs, diff --git a/world/zones.json b/world/zones.json index a4027ff..849d843 100644 --- a/world/zones.json +++ b/world/zones.json @@ -1,8 +1,9 @@ { - "_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).", + "_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). '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}, "species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt"}, "zones": [ {