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 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-30 01:50:56 +01:00
1 parent d105f6757d
commit 173203eeb5
10 files changed
+91 -3

No files matched your search

@@ -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],[Line truncated {"_comment":"Generated by tools/zone_layout.py from world/zones.json; do not edit by hand.","seed":"isle-26","radius":1700,"step":24,"size":142,"zones":[{"id":"button_isle","name":"Button Isle","levels":[2,10]},{"id":"frostpeak","name":"Frostpeak Range","levels":[40,55]},{"id":"sunscorch","name":"Sunscorch","levels":[30,45]},{"id":"mire","name":"Mangrove Mire","levels":[25,38]},{"id":"jungle","name":"Verdant Jungle","levels":[18,30]},{"id":"highlands","name":"Highland Meadows","levels":[25,35]},{"id":"westmarch","name":"Westmarch","levels":[15,25]},{"id":"southwold","name":"Southwold","levels":[8,18]},{"id":"shallows","name":"Shallows","levels":[5,20]},{"id":"deep_sea","name":"Deep Sea","levels":[30,50]}],"caves":[{"id":"lush_caves","name":"Lush Caves","levels":[12,28],"biome":"minecraft:lush_caves"},{"id":"dripstone","name":"Dripstone Depths","levels":[22,38],"biome":"minecraft:dripstone_caves"},{"id":"deep_dark","name":"Deep Dark","levels":[45,60],"biome":"minecraft:deep_dark"},{"id":"basalt","name":"Basalt Furnace","levels":[55,70],"biome":"minecraft:basalt_deltas"}],"level_pressure":{"grace_levels":5,"per_level":0.1,"minimum":0.1},"samples":{"button_isle":[-116,null,-92],"frostpeak":[628,null,-572],"sunscorch":[1372,null,-236],"mire":[652,null,1012],"jungle":[844,null,196],"highlands":[100,null,-1076],"westmarch":[-428,null,-1100],"southwold":[-116,null,1108],"shallows":[988,null,4],"deep_sea":[-1268,null,4],"lush_caves":[-1028,0,292],"dripstone":[940,40,-212],"deep_dark":[292,-52,-812],"basalt":[1276,0,508]},"biome_samples":{"frostpeak":[580,null,-524],"sunscorch":[-44,null,-452],"mire":[652,null,1012],"jungle":[844,null,196]},"grid_rle":[[9,771],[8,20],[9,117],[8,30],[9,109],[8,36],[9,103],[8,17],[5,1],[8,1],[5,5],[8,1],[5,3],[8,15],[9,97],[8,12],[5,1],[8,1],[5,22],[8,11],[9,87],[8,3],[9,5],[8,8],[6,5],[5,26],[1,1],[8,10],[9,82],[8,6],[9,3],[8,8],[6,6],[5,24],[1,3],[5,2],[8,10],[9,79],[8,8],[9,2],[8,6],[6,8],[5,31],[1,1],[8,9],[9,75],[8,15],[6,11],[5,21],[1,6],[5,7],[8,9],[9,71],[8,15],[6,1],[8,1],[6,11],[5,20],[1,9],[5,7],[8,9],[9,68],[8,14],[6,1],[8,1],[6,13],[5,19],[1,11],[5,9],[8,7],[9,65],[8,14],[6,1],[8,1],[6,15],[5,18],[1,11],[5,12],[8,7],[9,62],[8,13],[6,1],[8,1],[2,1],[6,16],[5,18],[1,9],[5,15],[8,7],[9,60],[8,12],[6,1],[8,1],[2,3],[6,16],[5,18],[1,6],[5,20],[8,7],[9,57],[8,11],[6,4],[2,2],[6,17],[5,46],[8,6],[9,54],[8,12],[6,5],[2,1],[6,19],[5,46],[8,6],[9,52],[8,11],[6,7],[2,1],[6,19],[5,25],[1,5],[5,18],[8,5],[9,50],[8,11],[6,27],[5,26],[1,6],[5,18],[8,5],[9,48],[8,12],[6,27],[5,24],[1,8],[5,19],[8,5],[9,46],[8,9],[2,1],[8,2],[2,2],[6,26],[5,24],[1,9],[5,19],[8,5],[9,44],[8,9],[2,6],[6,26],[5,24],[1,11],[5,18],[8,5],[9,42],[8,9],[2,7],[6,26],[5,23],[1,5],[5,1],[1,5],[5,20],[8,5],[9,41],[8,8],[2,8],[6,26],[5,23],[1,12],[5,20],[8,5],[9,40],[8,7],[2,8],[6,27],[5,23],[1,13],[5,20],[8,5],[9,40],[8,5],[2,10],[6,26],[5,26],[1,10],[5,21],[8,5],[9,38],[8,5],[2,9],[6,28],[5,27],[1,7],[5,24],[8,5],[9,36],[8,5],[2,9],[6,29],[5,28],[1,5],[5,26],[8,4],[9,35],[8,5],[6,2],[2,4],[6,33],[5,29],[1,3],[5,28],[8,4],[9,33],[8,5],[6,40],[5,61],[8,4],[9,32],[8,5],[6,40],[5,20],[1,1],[5,41],[8,4],[9,30],[8,5],[6,41],[5,16],[1,3],[5,1],[1,5],[5,37],[8,4],[9,29],[8,5],[6,42],[5,12],[1,18],[5,11],[1,1],[5,21],[8,4],[9,28],[8,4],[6,43],[5,12],[1,17],[5,3],[1,4],[5,2],[1,1],[5,2],[1,1],[5,22],[8,4],[9,26],[8,5],[6,43],[5,12],[1,18],[5,1],[1,5],[5,1],[1,3],[5,1],[1,1],[5,22],[8,4],[9,26],[8,4],[6,44],[5,12],[1,11],[5,1],[1,6],[5,1],[1,3],[5,2],[1,4],[5,25],[8,4],[9,24],[8,5],[6,43],[5,5],[1,1],[5,7],[1,13],[5,1],[1,2],[5,7],[1,2],[5,1],[1,2],[5,25],[8,4],[9,24],[8,5],[6,43],[5,4],[1,4],[5,5],[1,9],[5,2],[1,2],[5,9],[1,8],[5,23],[8,5],[9,22],[8,4],[2,3],[6,43],[5,1],[1,8],[5,3],[1,7],[5,4],[1,1],[5,9],[1,9],[5,24],[8,4],[9,22],[8,4],[2,3],[6,43],[5,1],[1,8],[5,4],[1,5],[5,3],[1,3],[5,9],[1,9],[5,25],[8,4],[9,20],[8,4],[2,4],[6,43],[1,9],[5,4],[1,10],[5,10],[1,9],[5,25],[8,4],[9,20],[8,4],[6,47],[1,10],[5,4],[1,3],[5,2],[1,3],[5,10],[1,10],[5,26],[8,4],[9,18],[8,4],[6,48],[1,10],[5,3],[2,1],[5,1],[2,2],[5,15],[1,9],[5,28],[8,2],[9,19],[8,4],[6,48],[1,9],[2,7],[5,16],[1,9],[5,4],[1,3],[5,21],[8,1],[9,19],[8,4],[6,48],[1,9],[2,8],[5,16],[1,9],[5,3],[1,4],[5,22],[8,1],[9,18],[8,4],[6,48],[1,7],[2,10],[5,16],[1,9],[5,3],[1,5],[5,20],[2,1],[8,1],[9,18],[8,3],[6,50],[1,5],[2,11],[5,16],[1,9],[5,3],[1,5],[5,19],[2,2],[8,2],[9,16],[8,4],[6,50],[5,2],[1,2],[2,13],[5,15],[1,10],[5,3],[1,4],[5,18],[2,3],[5,1],[8,1],[9,16],[8,3],[6,51],[5,3],[2,12],[1,4],[5,14],[1,10],[5,23],[2,4],[5,1],[8,2],[9,15],[8,3],[6,51],[5,3],[2,11],[1,5],[5,15],[1,9],[5,23],[2,5],[5,1],[8,2],[9,14],[8,3],[6,51],[5,3],[2,11],[1,1],[5,2],[1,3],[5,15],[1,10],[5,20],[2,6],[5,1],[8,3],[9,10],[8,1],[9,1],[8,4],[6,51],[5,3],[2,3],[9,3],[8,5],[1,1],[5,3],[1,1],[5,16],[1,12],[5,17],[2,7],[5,1],[8,4],[9,9],[8,5],[4,2],[6,50],[5,3],[2,1],[8,1],[9,5],[8,7],[5,1],[1,1],[5,16],[1,3],[5,5],[1,5],[5,15],[2,9],[8,6],[9,7],[8,5],[4,5],[6,47],[5,1],[8,1],[9,2],[8,1],[9,5],[8Line truncated
+1
View File
@@ -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 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 | | 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 | | 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` | | | Healing while sleeping | off (`healPercent` 0) | Decided (roadmap) | `config/cobblemon/main.json` | |
| Loot container refresh | 1 hour (72,000 ticks of game time) after first opened, all containers | Decided (Adrian, 2026-10-29) | `config/lootr-common.toml` | Tiering by structure/biome later | | Loot container refresh | 1 hour (72,000 ticks of game time) after first opened, all containers | Decided (Adrian, 2026-10-29) | `config/lootr-common.toml` | Tiering by structure/biome later |
| 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` | | 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` |
+4 -1
View File
@@ -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`. - **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 ## 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. - [ ] **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. - [ ] **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. - [ ] **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.
+2
View File
@@ -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`. **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): **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. - 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. - 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.
@@ -8,6 +8,7 @@ import dev.cobblesurvival.permadeath.Permadeath
import dev.cobblesurvival.selftest.SelfTest import dev.cobblesurvival.selftest.SelfTest
import dev.cobblesurvival.world.OnlyPokemon import dev.cobblesurvival.world.OnlyPokemon
import dev.cobblesurvival.zone.Guardians import dev.cobblesurvival.zone.Guardians
import dev.cobblesurvival.zone.LevelPressure
import dev.cobblesurvival.zone.ZoneCondition import dev.cobblesurvival.zone.ZoneCondition
import dev.cobblesurvival.zone.ZoneHazards import dev.cobblesurvival.zone.ZoneHazards
import dev.cobblesurvival.zone.Zones import dev.cobblesurvival.zone.Zones
@@ -59,6 +60,7 @@ class CobbleSurvival {
NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners) NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners)
Guardians.register() Guardians.register()
ZoneHazards.register() ZoneHazards.register()
LevelPressure.register()
SelfTest.register() SelfTest.register()
} }
@@ -1,6 +1,8 @@
package dev.cobblesurvival.selftest package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.Cobblemon 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.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnCause import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail 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.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.util.spawner import com.cobblemon.mod.common.util.spawner
import dev.cobblesurvival.CobbleSurvival import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.zone.LevelPressure
import dev.cobblesurvival.zone.Zone import dev.cobblesurvival.zone.Zone
import dev.cobblesurvival.zone.ZoneSample import dev.cobblesurvival.zone.ZoneSample
import dev.cobblesurvival.zone.Zones import dev.cobblesurvival.zone.Zones
@@ -16,7 +19,9 @@ import net.minecraft.core.BlockPos
import net.minecraft.gametest.framework.GameTestAssertException import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.server.level.ServerPlayer import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.Mth import net.minecraft.util.Mth
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.levelgen.Heightmap 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; * 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( fun all() = listOf(
SelfTestCase("spawn_pools_are_zoned", 20, ::spawnPoolsAreZoned), SelfTestCase("spawn_pools_are_zoned", 20, ::spawnPoolsAreZoned),
SelfTestCase("spawn_survey", 2400, ::spawnSurvey, islandOnly = true), 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('-')] 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. */ /** 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) { private fun spawnPoolsAreZoned(s: Scenario) {
val details = CobblemonSpawnPools.WORLD_SPAWN_POOL.details val details = CobblemonSpawnPools.WORLD_SPAWN_POOL.details
@@ -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())
}
}
@@ -19,6 +19,9 @@ import net.neoforged.neoforge.event.AddReloadListenerEvent
/** One zone of the island (world/zones.json). */ /** One zone of the island (world/zones.json). */
data class Zone(val id: String, val name: String, val minLevel: Int, val maxLevel: Int) 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. */ /** 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) 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<ResourceLocation, Zone> = emptyMap() private var caves: Map<ResourceLocation, Zone> = emptyMap()
var samples: Map<String, ZoneSample> = emptyMap() var samples: Map<String, ZoneSample> = emptyMap()
private set private set
var levelPressure = LevelPressureRules()
private set
/** For zones made of biomes: a point inside their own hazard biome (Frostpeak's jagged peaks, ...). */ /** For zones made of biomes: a point inside their own hazard biome (Frostpeak's jagged peaks, ...). */
var biomeSamples: Map<String, ZoneSample> = emptyMap() var biomeSamples: Map<String, ZoneSample> = emptyMap()
private set private set
@@ -58,6 +63,13 @@ object Zones : SimpleJsonResourceReloadListener(Gson(), "zones") {
val caveZones = json.getAsJsonArray("caves").map { it.asJsonObject } val caveZones = json.getAsJsonArray("caves").map { it.asJsonObject }
caves = caveZones.associate { ResourceLocation.parse(it.get("biome").asString) to zone(it) } caves = caveZones.associate { ResourceLocation.parse(it.get("biome").asString) to zone(it) }
byId = (byIndex + caves.values).associateBy { it.id } 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") samples = samplesOf(json, "samples")
biomeSamples = samplesOf(json, "biome_samples") biomeSamples = samplesOf(json, "biome_samples")
radius = json.get("radius").asInt radius = json.get("radius").asInt
+1
View File
@@ -312,6 +312,7 @@ def write_datapack(zone, plan, preview):
"seed": preview["seed"], "radius": preview["radius"], "step": preview["step"], "size": preview["size"], "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"]], "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"]], "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), "samples": samples(zone, plan, preview),
"biome_samples": biome_samples(zone, plan, preview), "biome_samples": biome_samples(zone, plan, preview),
"grid_rle": runs, "grid_rle": runs,
+2 -1
View File
@@ -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, "min_cells": 6,
"inherit": ["minecraft:river"], "inherit": ["minecraft:river"],
"guardian_respawn_hours": 5, "guardian_respawn_hours": 5,
"level_pressure": {"grace_levels": 5, "per_level": 0.1, "minimum": 0.1},
"species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt"}, "species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt"},
"zones": [ "zones": [
{ {