Add rifts, river basins and stratified depths for alpha.8
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.8 — 2026-09-09
|
||||
|
||||
- Forêts de surface conservées, avec de nouvelles failles courbes qui retirent
|
||||
de la roche pour ouvrir l’intérieur de l’île.
|
||||
- Nappes de tuf, cobblestone, pierre moussue et boue compactée dans les étages
|
||||
humides ; ardoise des abîmes, roche noire et basalte plus bas. Les poches de
|
||||
soufre et de cinabre peuvent apparaître à des altitudes plus accessibles.
|
||||
- Davantage d’essais de petits filons de charbon, fer et cuivre, y compris
|
||||
au contact de l’air. Ajout de l’émeraude et adaptation des huit familles de
|
||||
minerais aux nouvelles roches, sans quota de remplissage.
|
||||
- Décoration des corniches revue avec de gros champignons, des petits
|
||||
champignons plus dispersés et des sols de mousse, podzol et mycélium.
|
||||
- Bassins plus grands et recherche d’une
|
||||
longue rivière calme au tracé arrondi, de largeur variable, soutenue par la
|
||||
roche, avec bassins terminaux, berges progressives et source en paroi.
|
||||
- Recherche prioritaire de la poche de lave couverte entre Y=64 et Y=160,
|
||||
avec recherche plus haute si aucun site profond ne convient. Les sources
|
||||
profondes supplémentaires produisent leurs coulées par les ticks vanilla.
|
||||
- Nouvelle clé `sanctuary:sanctuary_rift` ; les anciennes générations jusqu’à
|
||||
l’alpha.7 restent séparées. Cet incrément nécessite un **nouveau monde Sanctuary**.
|
||||
|
||||
Validation : tests serveur réussis sur chacune des graines `0`, `42` et
|
||||
`8675309`, avec les bassins terminaux, la continuité des champignons et
|
||||
1 800 ticks réels de fluides. `check build assemblePack` réussi sur la graine
|
||||
`42`. Les mesures et leurs limites figurent dans [Validation](docs/testing.md).
|
||||
Les synchronisations packwiz sont vérifiées séparément lors de la distribution.
|
||||
Aucune rivière, coulée ou réserve de ressources n’est garantie pour chaque graine.
|
||||
|
||||
## 0.1.0-alpha.7 — 2026-09-08
|
||||
|
||||
- Forêts de chênes plus denses, y compris au cœur et sur les hauteurs de l’île,
|
||||
|
||||
@@ -23,14 +23,16 @@ décrits dans la vision ne sont pas encore implémentés.
|
||||
- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île
|
||||
flottante finie, un terrain naturel et un extérieur vide.
|
||||
- Île tempérée aux teintes douces : forêts de chênes et de bouleaux, clairières
|
||||
fleuries et affleurements de pierre, andésite, diorite et granite. Les corniches
|
||||
intérieures accueillent chênes noirs et champignons ; les essences rares
|
||||
restent ponctuelles. Les
|
||||
formes de terrain reprennent le générateur 26.2, adapté à l'API 26.3.
|
||||
- Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des
|
||||
altitudes adaptées au terrain relevé. Charbon, fer et cuivre peuvent aussi
|
||||
affleurer sur les roches de surface et les corniches. Les stocks se constatent
|
||||
après génération.
|
||||
fleuries et affleurements de pierre, andésite, diorite et granite. Des failles
|
||||
courbes ouvrent le terrain flottant issu du générateur 26.2. Les corniches
|
||||
humides accueillent chênes noirs et champignons ; les essences rares restent
|
||||
ponctuelles.
|
||||
- Géologie enrichie en profondeur : tuf, pierres moussues et boue compactée
|
||||
dans les intérieurs humides, puis ardoise des abîmes, roche noire, basalte et
|
||||
poches de soufre. Les volumes existants fournissent leur support.
|
||||
- Petits filons de charbon, fer, cuivre, or, redstone, lapis, diamant et émeraude
|
||||
à des altitudes adaptées au terrain. Les essais de charbon, fer et cuivre
|
||||
exposés sont plus fréquents. Les stocks se constatent après génération.
|
||||
- Spawn recherché sur le sol de l'île lors de la création. Les mondes ordinaires
|
||||
et les choix ultérieurs de spawn ne sont pas remplacés.
|
||||
- Nouveaux mondes sur 384 blocs (`Y=0..383`), île relevée de 64 blocs,
|
||||
@@ -39,12 +41,15 @@ décrits dans la vision ne sont pas encore implémentés.
|
||||
- Étangs, petits lacs, plages plus larges et dépôts de
|
||||
trois à cinq blocs de sable, gravier, pierre ou terre enherbée. La canne à
|
||||
sucre apparaît lorsque les rives respectent ses règles vanilla.
|
||||
- Les courts ruisseaux imposés sont retirés ; l’île garde ses étangs et petits
|
||||
lacs. La grande rivière traversante reste à développer.
|
||||
- Recherche de bassins plus grands et d’une longue rivière calme au tracé
|
||||
arrondi, avec largeur variable et berges progressives. La liaison de deux
|
||||
bassins, sa source et sa cascade entrante sont contrôlées dans les mondes
|
||||
de test de l’alpha.8.
|
||||
- Quelques sources dans des niches rocheuses naturelles, y compris aux étages
|
||||
inférieurs : Minecraft fait couler leur eau en cascades, qui peuvent descendre jusque dans le vide.
|
||||
- Une petite poche de lave couverte, accessible depuis une corniche naturelle,
|
||||
peut fournir la matière nécessaire à l’obsidienne lorsque le relief convient.
|
||||
- Une petite poche de lave couverte est recherchée en priorité sur une corniche
|
||||
profonde, puis plus haut si nécessaire. Une à deux sources profondes
|
||||
supplémentaires peuvent former des coulées, vérifiées par les ticks du moteur.
|
||||
|
||||
L'île a une emprise nominale de 512 blocs de diamètre, avec une bordure sculptée
|
||||
par des bruits liés à la seed. Aucune masse centrale n'est ajoutée pour forcer
|
||||
@@ -52,10 +57,14 @@ le terrain : le spawn cherche un emplacement naturel. Le catalogue
|
||||
TerraMix, les continents déverrouillables, les océans, les ruines et
|
||||
les autres fonctionnalités restent à construire par tickets.
|
||||
|
||||
L’alpha.7 utilise de nouveaux paramètres de génération ; créer un **nouveau monde**
|
||||
pour découvrir ses forêts, ses corniches humides et ses affleurements. Les sauvegardes alpha.2
|
||||
à alpha.6 conservent leur génération. Dappled Forest est désormais réservé aux
|
||||
futurs continents et reste présent dans les anciennes sauvegardes alpha.5. La validation de cet incrément est suivie dans [Validation](docs/testing.md).
|
||||
L’alpha.8 utilise la nouvelle génération `sanctuary:sanctuary_rift` ; créer un
|
||||
**nouveau monde** pour l’essayer. Les anciennes générations, jusqu’à l’alpha.7,
|
||||
gardent leurs paramètres ; aucune sauvegarde n’est régénérée. Dappled Forest
|
||||
reste réservé aux futurs continents et présent dans les anciennes sauvegardes
|
||||
alpha.5. **La validation moteur de l’alpha.8 passe sur les trois graines de
|
||||
référence**, avec 1 800 ticks de fluides ; aucune rivière, coulée ou quantité
|
||||
de ressources n’est garantie pour toutes les graines. Les observations et
|
||||
leurs limites figurent dans [Validation](docs/testing.md).
|
||||
|
||||
Le premier champ de recherche climatique est défini : nord froid, sud chaud,
|
||||
ouest sec et est humide, avec leurs combinaisons diagonales. Un outil de
|
||||
@@ -79,7 +88,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.7 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.8 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -106,7 +115,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.7.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.8.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
+54
-2
@@ -10,7 +10,9 @@ Le [document de vision](vision.md) conserve les idées à long terme. Ce backlog
|
||||
commit. Le prototype WG-02 et l'initialisation du spawn WG-03 sont implémentés
|
||||
et passent les tests serveur décrits dans [Validation](testing.md). Les essais
|
||||
visuels, multijoueurs et de redémarrage indiqués dans ce document restent à
|
||||
faire avant de clôturer tous leurs critères. WG-04 à WG-07 restent proposés.
|
||||
faire avant de clôturer tous leurs critères. WG-04 à WG-07 restent ouverts,
|
||||
avec les incréments de l’île initiale décrits ci-dessous. WG-08 est validé ;
|
||||
WG-09 a ses mesures moteur alpha.8 ; la distribution est suivie séparément.
|
||||
|
||||
Résultat visé : un client et un serveur Fabric compatibles peuvent ouvrir un monde Sanctuary, plusieurs joueurs y arrivent sur une même île sûre et les chunks extérieurs restent vides. La génération est reproductible et ses limites sont documentées.
|
||||
|
||||
@@ -139,7 +141,7 @@ des bassins, présence de roche élargie et sources de paroi dans les strates
|
||||
inférieures. Les écoulements sont contrôlés dans le moteur et les anciennes
|
||||
générations sont préservées.
|
||||
|
||||
**Ticket suivant — grande rivière facultative :** chercher un long parcours qui
|
||||
**Suite suivie dans WG-09 — grande rivière facultative :** chercher un long parcours qui
|
||||
épouse l’île, avec source, chute et bassin, rives de sable, gravier et argile.
|
||||
Le démontrer sur la vraie densité Minecraft puis après décoration et ticks de
|
||||
fluide, avec une graine et des coordonnées reproductibles. Les essais du
|
||||
@@ -200,6 +202,56 @@ effective sur les corniches inférieures. Cela ne livre pas encore les continent
|
||||
du moteur, dont les forêts, les minerais exposés et les eaux après 1 800 ticks.
|
||||
Les observations et leurs limites sont consignées dans [Validation](testing.md).
|
||||
|
||||
### WG-09 — Failles, rivière et profondeurs — alpha.8
|
||||
|
||||
**Branche :** `codex/rifts-rivers-and-depths`.
|
||||
|
||||
**But :** ouvrir l’intérieur de l’île, enrichir sa géologie et ses ressources
|
||||
de survie, tout en conservant les forêts de surface appréciées en alpha.7.
|
||||
|
||||
**Critères d’acceptation :**
|
||||
|
||||
- Une à trois traces de failles courbes, de longueur nominale 84 à 160 blocs
|
||||
et de largeur variable, retirent uniquement de la matière du relief initial.
|
||||
Leur intersection réelle avec la roche est montrée sur les graines testées.
|
||||
- Les forêts de chênes et de bouleaux restent présentes en surface. Des gros
|
||||
champignons, des petits champignons dispersés et des tapis de mousse, podzol
|
||||
et mycélium occupent les corniches compatibles de l’intérieur humide.
|
||||
- Les matières changent avec la profondeur, avec des nappes de tuf, cobblestone,
|
||||
pierre moussue et boue compactée, puis ardoise des abîmes, roche noire et basalte.
|
||||
Des poches de soufre réellement accessibles sont observées sur le terrain.
|
||||
- Charbon, fer et cuivre exposés sont plus faciles à repérer dans les relevés.
|
||||
L’émeraude est ajoutée ; or, diamant, redstone et lapis sont cherchés aux
|
||||
altitudes présentes. Aucun stock fixé n’est réinjecté après comptage.
|
||||
- Des bassins plus grands sont retenus. Une rivière facultative est démontrée
|
||||
par un chemin d’eau continu d’au moins 140 blocs, reliant deux bassins au
|
||||
même niveau avec source rocheuse et cascade entrante. Le tracé est arrondi
|
||||
à l’échelle de 16 à 32 blocs, avec largeur de lit variable d’environ 5 à 7
|
||||
blocs. L’incision du lit reste bornée à 16 blocs ; les berges de rivière
|
||||
s’étagent progressivement avec au plus 12 blocs retirés, contre 2 pour les
|
||||
autres rives. Le fond et les dépôts reposent sur le terrain existant.
|
||||
- La poche de lave couverte est recherchée en priorité sur une corniche basse
|
||||
entre Y=64 et Y=160 ; la recherche plus haute n’intervient que si aucun
|
||||
emplacement profond compatible n’est trouvé.
|
||||
- Une à deux sources profondes de lave sont recherchées lorsque le site est
|
||||
compatible ; leurs coulées viennent des ticks vanilla. Les supports, les
|
||||
eaux voisines et la végétation sont contrôlés après simulation réelle.
|
||||
- La nouvelle clé `sanctuary:sanctuary_rift` n’active ces traitements que pour
|
||||
les nouveaux mondes. Les données et comportements historiques restent séparés.
|
||||
- Les graines `0`, `42` et `8675309` disposent de mesures moteur explicites,
|
||||
avec fluides actifs pendant 1 800 ticks. Une absence de rivière ou de source
|
||||
est rapportée honnêtement ; un bassin isolé ne valide pas une rivière.
|
||||
- Après validation : build et pack vérifiés, publication immuable, deux
|
||||
synchronisations du canal packwiz et conservation de la même instance Prism,
|
||||
de ses sauvegardes et de ses réglages.
|
||||
|
||||
**Vérification moteur alpha.8 — 9 septembre 2026 :** les trois graines de
|
||||
référence passent les contrôles finaux, dont les deux bassins terminaux,
|
||||
les champignons connectés et 1 800 ticks de fluides. Les stocks et leurs
|
||||
périmètres sont consignés dans [Validation](testing.md). Le build complet est
|
||||
réussi ; publication et synchronisations sont suivies dans la procédure de
|
||||
distribution. Ce ticket local n’est pas une issue distante publiée.
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
+8
-2
@@ -19,7 +19,7 @@ Après les vérifications, le commit et le push de la branche du ticket, le scri
|
||||
de publication réalise les étapes de release et de canal ci-dessous :
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.7.mrpack
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.8.mrpack
|
||||
```
|
||||
|
||||
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
|
||||
@@ -38,7 +38,7 @@ La procédure complète, également utilisable manuellement :
|
||||
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.7/sanctuary-0.1.0-alpha.7.jar
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.8/sanctuary-0.1.0-alpha.8.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
@@ -131,6 +131,12 @@ L’alpha.7 sélectionne `sanctuary:sanctuary_woodland` pour ses forêts, ses
|
||||
roches variées et ses minerais affleurants. Les mondes alpha.6 gardent
|
||||
`sanctuary:sanctuary_layered` ; aucune régénération de chunks n’est effectuée.
|
||||
|
||||
L’alpha.8 sélectionne `sanctuary:sanctuary_rift` pour les failles, les grandes
|
||||
rivières, les champignons géants et la géologie des profondeurs. Créer un
|
||||
nouveau monde Sanctuary pour les découvrir. La même instance Prism reçoit
|
||||
le nouveau JAR par le canal stable ; ses sauvegardes et réglages sont conservés.
|
||||
Les mondes alpha.7 et antérieurs gardent leurs paramètres de génération.
|
||||
|
||||
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
|
||||
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
|
||||
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
|
||||
|
||||
+140
-2
@@ -64,7 +64,8 @@ Le framework déplace aussi le spawn vers une grille de tests située loin du ce
|
||||
Le test capture donc le spawn juste avant ce déplacement, après l'initialisation
|
||||
normale du monde, et examine les coordonnées absolues de l'île.
|
||||
|
||||
Les neuf tests Sanctuary vérifient :
|
||||
Les scénarios suivants couvrent le socle et les générations historiques ;
|
||||
les contrôles et résultats propres à l’alpha.8 figurent plus bas :
|
||||
|
||||
1. Le vrai générateur Sanctuary est chargé et le spawn collectif repose sur une
|
||||
surface déjà présente dans la densité naturelle, avec un sol plein de 3×3
|
||||
@@ -91,7 +92,7 @@ Les neuf tests Sanctuary vérifient :
|
||||
active et les minerais sont inspectés sur le terrain. Une réserve de lave
|
||||
présente conserve ses 18 sources, ses supports et son accès après simulation.
|
||||
9. Les anciennes clés gardent leur traitement d’origine. Seuls les nouveaux
|
||||
paramètres activent les forêts et les eaux de l’alpha.7.
|
||||
paramètres activent le traitement ajouté pour leur génération.
|
||||
|
||||
La recherche de spawn parcourt l'île finie, par anneaux de quatre blocs jusqu'à
|
||||
288 blocs du centre. Elle examine d'abord la hauteur brute pour éviter de
|
||||
@@ -188,6 +189,143 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue
|
||||
Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau,
|
||||
de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents.
|
||||
|
||||
## Contrôles et validation alpha.8 — 9 septembre 2026
|
||||
|
||||
Les trois essais utilisent des mondes neufs et le preset de production
|
||||
`sanctuary:sanctuary_rift`, avec Minecraft 26.3-pre-2, Java 25, Fabric Loader
|
||||
0.19.5 et Fabric API 0.160.0+26.3. Les journaux propres aux graines `0`, `42`
|
||||
et `8675309` terminent leurs tests requis et leur tâche Gradle sans échec.
|
||||
Le build complet `check build assemblePack` est exécuté sur la graine `42`.
|
||||
Il passe en 4m 57s,
|
||||
avec 12 tests purs de comportement et les tests serveur.
|
||||
Les exports généraux facultatifs sont désactivés ; les diagnostics ciblés et
|
||||
les assertions de comportement restent actifs.
|
||||
|
||||
Les nouveaux contrôles exigent de vrais bassins terminaux connectés à la
|
||||
rivière et la continuité physique entre chaque tige et son chapeau de gros
|
||||
champignon. Les rapports antérieurs à ces contrôles ne sont pas utilisés.
|
||||
|
||||
| Graine | Chunks FULL | Failles / colonnes témoins | Chênes / bouleaux supérieurs | Chênes noirs inférieurs | Gros champignons | Petits sous plafond | Minerais dehors : charbon / fer / cuivre |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 246 | 3 / 3 | 331 / 249 | 19 | 22 | 273 | 488 / 170 / 124 |
|
||||
| `42` | 216 | 2 / 2 | 361 / 178 | 17 | 15 | 140 | 379 / 209 / 148 |
|
||||
| `8675309` | 231 | 3 / 3 | 236 / 125 | 31 | 17 | 202 | 633 / 261 / 262 |
|
||||
|
||||
Les mêmes échantillons montrent la coexistence de chênes noirs et de gros
|
||||
champignons sur les graines `0`, `42`, `8675309`.
|
||||
|
||||
Les arbres sont comptés par groupes de pieds de troncs avec sol et feuilles
|
||||
observés. Les gros champignons ont une tige enracinée, un chapeau connecté et
|
||||
une voûte naturelle ; leurs blocs sont de nouveau contrôlés après les fluides.
|
||||
Les petits champignons sous plafond ont également leurs supports naturels.
|
||||
Ils ne sont pas confondus avec le sous-ensemble `actual_lower_terrace_vegetation`.
|
||||
Les failles sont vérifiées sur des colonnes témoins à l’intérieur des traces :
|
||||
ce nombre ne mesure pas la longueur complète des canyons.
|
||||
|
||||
| Graine | Longueur rivière | Déplacement | Niveau Y | Colonnes d’eau | Colonnes des deux bassins | Sources liées | Descente liée observée |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 409,8 | 199,7 | 243 | 2 863 | 240 / 241 | 1 | 8 |
|
||||
| `42` | 247,8 | 171,0 | 247 | 1 712 | 254 / 247 | 1 | 6 |
|
||||
| `8675309` | 340,7 | 251,3 | 239 | 2 247 | 265 / 244 | 1 | 8 |
|
||||
|
||||
La longueur vient de `river_routes[].length`, somme des déplacements successifs
|
||||
du chemin, diagonales comprises. Le déplacement est la distance entre ses
|
||||
extrémités. Les deux bassins ont des élargissements mesurés au-delà du lit ;
|
||||
chaque raccord local et l’arrivée réelle de la cascade sont contrôlés. Le
|
||||
tracé est arrondi à l’échelle de 16 à 32 blocs, avec un lit d’environ 5 à 7
|
||||
blocs de large. Les limites sont de 16 blocs retirés pour le lit, 12 pour ses
|
||||
berges progressives et 2 pour les autres rives, sur support naturel.
|
||||
|
||||
| Graine | Eau retenue | Eau hors bassins | Eau totale | Lave totale | Sources de lave | Coulées déclarées | Accès à la poche de lave |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `0` | 6 463 | 363 | 6 826 | 144 | 20 | 2 | `(-5, 106, 80)` |
|
||||
| `42` | 4 899 | 358 | 5 257 | 144 | 20 | 2 | `(-35, 108, 6)` |
|
||||
| `8675309` | 5 841 | 345 | 6 186 | 144 | 20 | 2 | `(-55, 132, 0)` |
|
||||
|
||||
Ces fluides sont lus après 1 800 ticks dans des chunks actifs, avec témoins
|
||||
indépendants du fonctionnement des ticks. Les volumes retenus correspondent
|
||||
exactement aux volumes attendus. L’eau extérieure reste reliée aux sources
|
||||
déclarées ; la lave des coulées est distinguée des sources de la poche couverte.
|
||||
La poche contient 18 sources lorsqu’elle est présente ; ses coordonnées
|
||||
d’accès ne sont pas les niveaux des blocs de lave. Sa recherche privilégie
|
||||
Y=64..160, avec un site plus haut possible si la recherche basse échoue.
|
||||
Les coulées supplémentaires partent de sources entre Y=40 et Y=160. Ce relevé
|
||||
à 1 800 ticks ne signifie pas que tout écoulement a atteint son état final.
|
||||
|
||||
| Graine | Sols de mousse | Sols de podzol | Sols de mycélium |
|
||||
| --- | --- | --- | --- |
|
||||
| `0` | 16 268 | 15 441 | 6 855 |
|
||||
| `42` | 7 197 | 6 684 | 3 502 |
|
||||
| `8675309` | 12 519 | 10 616 | 5 185 |
|
||||
|
||||
Les sols sont des colonnes inférieures naturelles réellement revêtues de ces
|
||||
matières. Les proportions de placement ne sont pas imposées au relevé.
|
||||
|
||||
Les stocks suivants comptent les blocs dans les seuls chunks inspectés,
|
||||
**avant les ticks de fluides**. Ils incluent les variantes ordinaires et
|
||||
d’ardoise de chaque minerai. Ils ne représentent pas toute l’île et ne sont
|
||||
jamais complétés pour atteindre un quota.
|
||||
|
||||
| Minerai | Graine 0 | Graine 42 | Graine 8675309 |
|
||||
| --- | --- | --- | --- |
|
||||
| Charbon | 7 291 | 5 738 | 4 295 |
|
||||
| Fer | 2 899 | 2 330 | 1 860 |
|
||||
| Cuivre | 2 229 | 1 804 | 1 416 |
|
||||
| Or | 829 | 533 | 425 |
|
||||
| Redstone | 819 | 534 | 432 |
|
||||
| Lapis | 237 | 229 | 122 |
|
||||
| Diamant | 126 | 80 | 71 |
|
||||
| Émeraude | 55 | 34 | 30 |
|
||||
|
||||
| Matière | Graine 0 | Graine 42 | Graine 8675309 |
|
||||
| --- | --- | --- | --- |
|
||||
| Tuf | 562 892 | 439 519 | 264 881 |
|
||||
| Cobblestone | 288 921 | 140 182 | 73 681 |
|
||||
| Cobblestone moussue | 452 727 | 381 339 | 220 767 |
|
||||
| Boue compactée | 192 049 | 145 758 | 101 652 |
|
||||
| Ardoise des abîmes | 1 437 063 | 712 417 | 807 405 |
|
||||
| Roche noire | 379 527 | 194 967 | 248 247 |
|
||||
| Basalte | 549 851 | 293 938 | 137 965 |
|
||||
| Soufre | 462 657 | 313 445 | 401 190 |
|
||||
| Cinabre | 118 411 | 74 761 | 114 313 |
|
||||
|
||||
Ces stocks ne prouvent pas seuls l’accessibilité de chaque bloc. Les cartes
|
||||
de surfaces, les positions des minerais exposés et les témoins de corniches
|
||||
complètent le relevé. Les minerais « dehors » ont une vraie face d’air ouverte
|
||||
vers le ciel ou une ouverture horizontale naturelle inspectée sur douze blocs.
|
||||
Le tableau présente le charbon, le fer et le cuivre. Les JSON conservent aussi
|
||||
les autres familles effectivement recensées dehors, comme l’émeraude lorsqu’elle
|
||||
est observée ; le choix des colonnes ne limite pas le périmètre du diagnostic.
|
||||
L’échantillon couvre les eaux et leurs voisins, des cibles écologiques et le
|
||||
site facultatif d’arbre remarquable ; il est volontairement orienté par ces
|
||||
sites, sans prétendre donner un pourcentage représentatif de l’île.
|
||||
|
||||
| Graine | Réentrée bouche d’eau | Réentrée bouche de lave |
|
||||
| --- | --- | --- |
|
||||
| `0` | 1 vérifiée(s) | 1 vérifiée(s) |
|
||||
| `42` | 1 vérifiée(s) | 1 vérifiée(s) |
|
||||
| `8675309` | 1 vérifiée(s) | 1 vérifiée(s) |
|
||||
|
||||
Après consommation des marqueurs de génération, le rappel du post-traitement
|
||||
conserve les modifications temporaires appliquées aux bouches testées ; les
|
||||
blocs témoins sont ensuite restaurés. Les 113 fichiers historiques
|
||||
suivis conservent leurs SHA-256. Aucun monde personnel n’est ouvert par ces tests.
|
||||
|
||||
Preuves locales ignorées par Git : `build/alpha8-build.log` (build complet sur
|
||||
42), `build/alpha8-seed0.log`, `build/alpha8-seed42.log`,
|
||||
`build/alpha8-seed8675309.log`, les JSON `rift-seed-<graine>-after-1800-ticks`
|
||||
et `-postprocessing-reentry`, les cartes de canopée et de corniches et les
|
||||
profils de cascades sous `mods/sanctuary/build/run/gameTest/diagnostics/`.
|
||||
`build/report-alpha8-validation.py` extrait leurs champs sans remplacer les
|
||||
données absentes par des résultats supposés. Le hash du JAR vérifié est
|
||||
conservé dans `build/alpha8-jar-sha256.txt`.
|
||||
|
||||
Ces résultats portent sur les graines testées. Ils ne garantissent aucune
|
||||
rivière, essence rare ou quantité de ressources sur toutes les graines.
|
||||
L’appréciation visuelle du paysage et l’équilibrage en partie restent ouverts.
|
||||
Les deux synchronisations packwiz et la conservation de l’instance Prism
|
||||
doivent être vérifiées lors de la distribution, selon [Distribution Prism](packwiz.md).
|
||||
|
||||
## Contrôles et validation alpha.7 — 8 septembre 2026
|
||||
|
||||
`WoodlandDiagnostics` reprend les preuves hydrologiques et ajoute les forêts,
|
||||
|
||||
+24
-6
@@ -70,13 +70,23 @@ L’ambiance de départ doit rester douce et tempérée, avec de véritables for
|
||||
et des clairières fleuries, quelques zones plus sèches et de larges masses
|
||||
rocheuses mêlant pierre, andésite, diorite et granite. Les nappes de sable et
|
||||
de gravier accompagnent les eaux. Charbon, fer et cuivre doivent aussi pouvoir
|
||||
affleurer sur la roche exposée pour commencer la survie.
|
||||
affleurer sur la roche exposée pour commencer la survie, avec du charbon assez
|
||||
repérable pour fournir les premières expériences. Les failles doivent ouvrir
|
||||
des passages courbes et des vues vers les profondeurs, sans reconstruire une
|
||||
masse artificielle au centre de l’île.
|
||||
|
||||
La géologie change en descendant : tuf, cobblestone, pierres moussues et boue
|
||||
compactée dans les intérieurs humides, puis ardoise des abîmes, roche noire et
|
||||
basalte. Des poches de soufre doivent être réellement explorables. Quelques
|
||||
sources profondes peuvent former des coulées de lave ; elles ne doivent pas
|
||||
transformer les profondeurs en grands bassins remplis. L’émeraude rejoint les
|
||||
minerais à rechercher, avec l’or, le diamant, la redstone et le lapis.
|
||||
|
||||
Les bois dominants sont le chêne, le chêne noir et le bouleau : chênes et
|
||||
bouleaux en surface, chênes noirs et champignons dans l’intérieur plus humide,
|
||||
avec moins de mousse uniforme. Épicéa, jungle, acacia, palétuvier et chêne pâle
|
||||
restent rares mais possibles ; du bambou et de rares poches de soufre peuvent
|
||||
se rencontrer dans les profondeurs. Un arbre remarquable isolé peut être un
|
||||
bouleaux en surface, chênes noirs et gros champignons dans l’intérieur plus
|
||||
humide, avec de petits champignons dispersés et des tapis variés de mousse,
|
||||
podzol et mycélium. Épicéa, jungle, acacia, palétuvier et chêne pâle restent rares
|
||||
mais possibles ; du bambou peut se rencontrer dans les profondeurs. Un arbre remarquable isolé peut être un
|
||||
cerisier rose ou une autre de ces essences rares, lorsque le terrain convient.
|
||||
Les biomes très contrastés de Minecraft 26.3, dont Dappled Forest, restent
|
||||
réservés aux futurs continents.
|
||||
@@ -86,7 +96,15 @@ en quantités modestes pour pousser à explorer les expansions lors des grands
|
||||
projets. Les minerais doivent être trouvables aux altitudes du terrain. Les
|
||||
stocks sont observés après génération et les ressources renouvelables gardent
|
||||
leurs règles Minecraft. Le réglage précis de ces quantités reste à éprouver en
|
||||
partie coopérative. L’état livré figure dans [Génération](worldgen.md).
|
||||
partie coopérative. La recherche de l’alpha.8 porte notamment sur une rivière
|
||||
calme continue reliant deux bassins au même niveau, alimentée par une source
|
||||
en paroi et sa cascade. Son tracé doit former de larges courbes, avec une
|
||||
largeur variable et des berges progressives. La petite réserve de lave est
|
||||
recherchée d’abord dans les étages profonds ; un site plus haut reste possible
|
||||
si la roche ne fournit pas de niche compatible en dessous. Ces formes restent
|
||||
facultatives selon le relief ; leur existence doit être démontrée en jeu.
|
||||
L’état de chaque incrément et ses limites
|
||||
figurent dans [Génération](worldgen.md).
|
||||
|
||||
Le premier champ de recherche des expansions associe les **huit directions**
|
||||
autour de Sanctuary à des signatures de température et d'humidité. En regardant
|
||||
|
||||
+150
-5
@@ -5,6 +5,11 @@ entourée de vide sans répétition d'îles à grande distance. C'est une premi
|
||||
tranche de génération pour Minecraft **26.3-pre-2 / Fabric**, pas encore le
|
||||
système d'expansion collective.
|
||||
|
||||
L’alpha.8 utilise les paramètres `sanctuary:sanctuary_rift`. Ses contrôles
|
||||
dans les vrais chunks passent sur les trois graines de référence, avec
|
||||
1 800 ticks de fluides. Les observations et limites de cette version sont
|
||||
consignées séparément des générations précédentes dans [Validation](testing.md).
|
||||
|
||||
## Créer un monde
|
||||
|
||||
En solo : créer un **nouveau** monde et choisir le type de monde **Sanctuary**
|
||||
@@ -29,13 +34,14 @@ de test et les sauvegardes personnelles doivent rester distincts.
|
||||
avec `xz_scale=0.25`, `y_scale=0.25`, `xz_factor=80`, `y_factor=160`, `smear=4`,
|
||||
ainsi que son gradient supérieur. Il est échantillonné **64 blocs plus bas**
|
||||
pour relever l'île entière de 64 blocs. Reliefs, surplombs, trous et fragments
|
||||
dépendent de la seed Minecraft. L'ancien gradient inférieur uniforme est
|
||||
dépendent de la seed Minecraft. L’alpha.8 y soustrait des failles courbes,
|
||||
sans ajouter de matière. L'ancien gradient inférieur uniforme est
|
||||
remplacé par la sculpture irrégulière décrite ci-dessous.
|
||||
- Le terrain naturel reste libre de laisser un trou à l'origine, de former des
|
||||
surplombs ou de se séparer en fragments. **Aucun noyau ellipsoïdal, cylindre ou
|
||||
plateforme de départ n'est ajouté.** Au-dessus de `Y=144`, le relief de la
|
||||
génération précédente est conservé dans la densité, simplement relevé de 64
|
||||
blocs. L’hydrologie traite ensuite les lits et les rives près de la surface.
|
||||
plateforme de départ n'est ajouté.** Le relief relevé de 64 blocs reste le
|
||||
champ source ; les failles de l’alpha.8 en retirent de la matière et
|
||||
l’hydrologie traite ensuite les lits et les rives près de la surface.
|
||||
- Le pourtour est sculpté par retrait de matière à partir du rayon **128 blocs**.
|
||||
Le contour de référence de 256 blocs est déformé par les bruits, entre 204 et
|
||||
288 blocs ; il indique la fin de l'érosion, pas un mur de roche.
|
||||
@@ -58,13 +64,152 @@ de test et les sauvegardes personnelles doivent rester distincts.
|
||||
rocheuses. Les arbres et les petits filons ont besoin du terrain existant.
|
||||
Aucun mod de biomes externe n’est nécessaire.
|
||||
|
||||
L’alpha.7 associe ces biomes aux plages, aux eaux et aux petits filons décrits
|
||||
L’alpha.8 associe ces biomes aux plages, aux eaux et aux filons décrits
|
||||
ci-dessous. Les ressources de survie ne se limitent pas au bois et à la pierre,
|
||||
mais cet incrément ne garantit pas encore une partie Minecraft complète sur
|
||||
chaque seed. L’accès à chaque progression vanilla et les ressources renouvelables
|
||||
nécessitent des vérifications concrètes. Les stocks réels sont relevés après la
|
||||
génération, sans corriger une île pour atteindre un quota.
|
||||
|
||||
## Failles, géologie et ressources — génération v8
|
||||
|
||||
Les nouveaux mondes utilisent `sanctuary:sanctuary_rift`, la densité
|
||||
`sanctuary:final_density_rift` et le biome source `sanctuary:rift_island`.
|
||||
Les anciens paramètres `sanctuary`, `sanctuary_384`, `sanctuary_hydrology`,
|
||||
`sanctuary_natural`, `sanctuary_layered` et `sanctuary_woodland` restent séparés.
|
||||
Changer le preset ne migre pas un monde existant ; aucune régénération de
|
||||
chunks ni modification de sauvegarde n’est prévue par cet incrément.
|
||||
|
||||
### Failles et palette
|
||||
|
||||
`RiftShape` définit une à trois traces principales, courbes et ramifiées,
|
||||
de longueur nominale 84 à 160 blocs. Leur largeur varie avec deux bruits en
|
||||
trois dimensions ; le cœur ouvert est de l’ordre de 4 à 12 blocs, avec des
|
||||
parois érodées autour. Ces nombres décrivent les traces avant leur intersection
|
||||
avec la roche, pas une garantie de canyon continu de cette taille. La passe
|
||||
retire seulement de la matière et laisse intactes les zones de rayon inférieur
|
||||
à 20 blocs et extérieur à 224 blocs. Elle ne crée aucun socle central.
|
||||
|
||||
Les huit biomes `rift_oak_forest`, `rift_birch_forest`, `rift_clearing`,
|
||||
`rift_dry_woodland`, `rift_rocky_heath`, `rift_dark_grove`, `rift_bamboo_grove`
|
||||
et `rift_sulfur_depths` ont de nouveaux identifiants dans le namespace
|
||||
`sanctuary`. Les choix climatiques de l’alpha.7 sont conservés en haut. Le
|
||||
soufre devient admissible dans des régions plus larges des étages inférieurs,
|
||||
notamment vers Y=176, selon les bruits de variation et d’humidité. Le test de
|
||||
palette prouve cette possibilité ; seule la lecture des vrais blocs peut
|
||||
prouver une poche accessible sur une graine.
|
||||
|
||||
La végétation vanilla de surface garde ses arbres et ses teintes. La feature
|
||||
`rift_groves` passe ensuite, avant `rift_shore_sugar_cane`, pour choisir les
|
||||
volumes inférieurs encore libres après les arbres. Elle peut placer de gros
|
||||
champignons vanilla, avec des petits champignons dispersés et des sols mêlant
|
||||
mousse, podzol et mycélium. Les supports naturels et la place disponible
|
||||
restent nécessaires ; un arbre remarquable peut échouer si son site est occupé.
|
||||
L’ordre des plantations intérieures donne la priorité aux arbres dans trois
|
||||
cas sur quatre et aux gros champignons dans l’autre cas, selon un choix lié
|
||||
au chunk. Cela laisse des volumes disponibles aux chênes noirs. Dans les
|
||||
biomes `sanctuary:rift_dark_grove` et `sanctuary:rift_bamboo_grove`, l’ardoise
|
||||
des abîmes peut aussi fournir le support naturel des sols et bosquets
|
||||
inférieurs. Son admissibilité est vérifiée au-dessus de chaque pied : seules
|
||||
ces deux variantes humides autorisent ce support.
|
||||
|
||||
### Matières selon la profondeur
|
||||
|
||||
La règle `sanctuary:rift_island` ne change que la matière de blocs déjà solides.
|
||||
Ses nappes utilisent un bruit 3D, avec un bruit distinct pour faire varier les
|
||||
transitions de profondeur.
|
||||
|
||||
| Domaine | Matières |
|
||||
| --- | --- |
|
||||
| Masses supérieures | Pierre, granite, andésite, diorite ; sols enherbés hors affleurements |
|
||||
| Intérieur des bosquets humides | Tuf, cobblestone, cobblestone moussue, boue compactée |
|
||||
| Masses profondes | Ardoise des abîmes, roche noire, basalte |
|
||||
| Poches soufrées | Soufre et cinabre ordinaires sur les masses existantes |
|
||||
|
||||
Les strates humides se mêlent aux matières hautes entre Y=192 et Y=224 dans
|
||||
leurs biomes ; la transition vers les roches profondes s’étend de Y=136 à Y=168.
|
||||
La profondeur sert de critère géologique : ces blocs ne signifient pas qu’une
|
||||
source de lave se trouve forcément à proximité. Aucun bassin, spike, source
|
||||
vanilla ou bloc de soufre puissant du biome Sulfur Caves n’est ajouté.
|
||||
|
||||
Les anciens petits dépôts vanilla de granite, andésite, diorite et tuf sont
|
||||
remplacés par ces nappes dans les seuls biomes `rift_*`. Les anciennes données
|
||||
de biomes et leurs features ne sont pas modifiées.
|
||||
|
||||
### Minerais
|
||||
|
||||
Les features `rift_ore_*` ciblent des tags Sanctuary locaux : les pierres hautes
|
||||
et humides produisent les minerais ordinaires ; ardoise des abîmes, roche noire
|
||||
et basalte produisent leurs variantes d’ardoise. Les tags vanilla ne sont pas
|
||||
élargis globalement. Le soufre et le cinabre restent des matières distinctes.
|
||||
|
||||
| Minerai | Taille du filon configurée | Essais principaux par chunk | Altitudes principales |
|
||||
| --- | ---: | --- | --- |
|
||||
| Charbon | 9 | 8 | Y=96..304 |
|
||||
| Fer | 7 | 4 | Y=88..280 |
|
||||
| Cuivre | 7 | 3 | Y=96..280 |
|
||||
| Or | 5 | 2 | Y=88..224 |
|
||||
| Redstone | 5 | 2 | Y=88..216 |
|
||||
| Lapis | 4 | 1 | Y=104..240 |
|
||||
| Diamant | 4 | Un essai une fois sur deux | Y=80..208 |
|
||||
| Émeraude | 3 | 1 | Y=104..224 |
|
||||
|
||||
Les hauteurs suivent une distribution en trapèze. Charbon, fer et cuivre
|
||||
reçoivent aussi respectivement 4, 2 et 2 essais sur roche supérieure exposée,
|
||||
et autant de recherches de corniche depuis l’air entre Y=64 et Y=288. Un essai
|
||||
peut ne produire aucun bloc. Les tailles configurées ne sont ni des quantités
|
||||
garanties par filon ni des quotas par île. L’exposition à l’air est autorisée
|
||||
pour ces trois minerais et l’émeraude ; une partie des candidats exposés est
|
||||
encore écartée pour les autres minerais. Les stocks devront être relevés après
|
||||
génération, avec leur emprise explicite.
|
||||
|
||||
### Eaux et lave
|
||||
|
||||
`RiftHydrology` cherche des bassins plus grands et une longue liaison calme
|
||||
sur la vraie densité fissurée. Le critère de cet incrément est un chemin d’eau
|
||||
continu d’au moins 140 blocs entre deux bassins au même niveau, alimenté par
|
||||
une source rocheuse et sa cascade entrante. Il ne s’agit pas d’une rivière
|
||||
descendant continuellement en altitude. La recherche parcourt huit directions
|
||||
et pondère les trajets selon le relief et un bruit ; le parcours retenu est
|
||||
lissé sur une échelle de 16 à 32 blocs, puis arrondi avant sa conversion en
|
||||
blocs. Le lit varie entre environ 5 et 7 blocs de largeur, hors élargissement
|
||||
des bassins. Le chemin et ses marges sont à nouveau vérifiés sur la roche
|
||||
après ce lissage : une courbe ne peut pas servir à franchir un trou.
|
||||
|
||||
La roche doit soutenir le fond et les berges. L’incision du lit reste limitée
|
||||
à 16 blocs. Les berges de rivière peuvent retirer jusqu’à 12 blocs pour
|
||||
rejoindre l’eau par paliers progressifs ; les autres rives conservent leur
|
||||
limite de 2 blocs. Les dépôts de 3 à 5 couches ont toujours besoin de supports
|
||||
naturels. Aucun remblai ne franchit une faille ou un trou. Si aucun parcours
|
||||
compatible n’est retenu, le générateur garde les bassins et sources qui
|
||||
conviennent au relief.
|
||||
|
||||
Le calcul initial du plan reste coûteux : les essais finaux sur ce poste
|
||||
mesurent 80,5 secondes pour la graine `0`, 50,7 pour `42` et 59,7 pour
|
||||
`8675309`, hors génération des chunks. Le résultat est mis en cache par état
|
||||
aléatoire du monde ; il n’est pas recalculé pour chaque chunk. L’optimisation
|
||||
de ce premier calcul reste un travail ultérieur.
|
||||
|
||||
La recherche de lave profonde vise une à deux petites sources entre Y=40 et
|
||||
Y=160, en complément de la niche couverte accessible. Cette poche de 18 blocs
|
||||
sources est recherchée d’abord sur une corniche intérieure entre Y=64 et
|
||||
Y=160. Si aucun site profond ne réunit les conditions de support, de voûte
|
||||
et d’accès, la recherche reprend sur les corniches plus hautes, jusqu’à Y=300.
|
||||
Il n’y a pas de poche créée de force lorsque les deux recherches échouent.
|
||||
Les sources supplémentaires laissent Minecraft former leurs coulées par ses
|
||||
ticks, sans colonne préremplie. L’eau, les supports et les marges de végétation doivent être
|
||||
contrôlés après décoration et simulation. La finition des bouches doit rester
|
||||
limitée à la première génération du chunk, sans effacer un aménagement joueur
|
||||
lors d’un rechargement.
|
||||
|
||||
**Validation du 9 septembre 2026 :** le build complet réussit et les contrôles
|
||||
moteur passent sur les trois graines de référence. Les failles, les bassins terminaux,
|
||||
les liaisons de rivière, les champignons et les fluides après 1 800 ticks sont
|
||||
documentés avec les relevés de ressources dans [Validation](testing.md).
|
||||
Ces échantillons ne garantissent pas la présence de chaque ressource sur
|
||||
toutes les graines. Les rapports intermédiaires ne servent pas de preuve
|
||||
pour la version finale ; aucun monde personnel n’est utilisé par ces tests.
|
||||
|
||||
## Sculpture du pourtour — conservée depuis la génération v2
|
||||
|
||||
La version **0.1.0-alpha.2** retire deux contraintes de la génération v1
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.7
|
||||
pack_version=0.1.0-alpha.7
|
||||
mod_version=0.1.0-alpha.8
|
||||
pack_version=0.1.0-alpha.8
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -147,3 +147,35 @@ tasks.register('woodlandGroveSmoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.WoodlandGroveSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('woodlandPaletteSmoke', 'woodlandGroveSmoke') }
|
||||
|
||||
tasks.register('riftShapeSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftShapeSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftShapeSmoke') }
|
||||
|
||||
tasks.register('riftPaletteSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftPaletteSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftPaletteSmoke') }
|
||||
|
||||
tasks.register('riftHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftHydrologySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftHydrologySmoke') }
|
||||
|
||||
tasks.register('riftLavaSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftLavaSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftLavaSmoke') }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+17
-3
@@ -4,6 +4,7 @@ import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.IslandShape;
|
||||
@@ -31,7 +32,8 @@ public final class SanctuaryWorldGameTests {
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
|
||||
if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
|
||||
else HydrologyDiagnostics.start(helper);
|
||||
@@ -40,7 +42,8 @@ public final class SanctuaryWorldGameTests {
|
||||
@GameTest(maxTicks = 200)
|
||||
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
|
||||
if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
else HydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
@@ -121,13 +124,24 @@ public final class SanctuaryWorldGameTests {
|
||||
if (chunk.getSection(0).hasOnlyAir()) continue;
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator)
|
||||
|| WoodlandHydrologyRuntime.enabled(generator),
|
||||
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.enabled(generator),
|
||||
"The legacy island must leave its lower margin entirely empty");
|
||||
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) {
|
||||
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
BlockPos p = new BlockPos(x, y, z);
|
||||
var state = chunk.getBlockState(p);
|
||||
if (RiftHydrologyRuntime.enabled(generator)) {
|
||||
boolean water = state.getFluidState().is(FluidTags.WATER)
|
||||
&& RiftDiagnostics.isWithinSpringInspectionFootprint(RiftHydrologyRuntime.riftPlan(helper.getLevel()), x, z);
|
||||
final int bx = x, bz = z;
|
||||
boolean lava = state.getFluidState().is(FluidTags.LAVA)
|
||||
&& RiftHydrologyRuntime.lavaPlan(helper.getLevel()).flowBounds().stream().anyMatch(b ->
|
||||
bx >= (Math.floorDiv(b.minX(),16) - 1) * 16 && bx <= (Math.floorDiv(b.maxX(),16) + 2) * 16 - 1
|
||||
&& bz >= (Math.floorDiv(b.minZ(),16) - 1) * 16 && bz <= (Math.floorDiv(b.maxZ(),16) + 2) * 16 - 1);
|
||||
helper.assertTrue(state.isAir() || water || lava, "Lower margin must remain void except declared fluid falls: " + p);
|
||||
continue;
|
||||
}
|
||||
helper.assertTrue(state.isAir() || state.getFluidState().is(FluidTags.WATER)
|
||||
&& (WoodlandHydrologyRuntime.enabled(generator)
|
||||
? WoodlandDiagnostics.isWithinSpringInspectionFootprint(WoodlandHydrologyRuntime.woodlandPlan(helper.getLevel()), x, z)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package fr.koka.sanctuary;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.RiftIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.RiftIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.RiftGrovesFeature;
|
||||
import fr.koka.sanctuary.worldgen.RiftShoreSugarCaneFeature;
|
||||
import fr.koka.sanctuary.worldgen.MainIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandIslandBiomeSource;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandShoreSugarCaneFeature;
|
||||
@@ -37,6 +41,10 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("woodland_island"), WoodlandIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("woodland_shore_sugar_cane"), WoodlandShoreSugarCaneFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("woodland_groves"), WoodlandGrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("rift_island"), RiftIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("rift_island"), RiftIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("rift_groves"), RiftGrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("rift_shore_sugar_cane"), RiftShoreSugarCaneFeature.CODEC);
|
||||
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.RiftSpringOutlets;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */
|
||||
@Mixin(LevelChunk.class)
|
||||
public abstract class RiftSpringOutletMixin {
|
||||
@Unique
|
||||
private RiftSpringOutlets.Pending sanctuary$pendingRiftOutlets = RiftSpringOutlets.Pending.EMPTY;
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
|
||||
private void sanctuary$captureRiftOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
sanctuary$pendingRiftOutlets = RiftSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
|
||||
}
|
||||
|
||||
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
|
||||
private void sanctuary$finishRiftOutlets(ServerLevel level, CallbackInfo callback) {
|
||||
var pending = sanctuary$pendingRiftOutlets;
|
||||
sanctuary$pendingRiftOutlets = RiftSpringOutlets.Pending.EMPTY;
|
||||
RiftSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import fr.koka.sanctuary.worldgen.HydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.minecraft.core.Holder;
|
||||
@@ -28,7 +29,12 @@ public abstract class SurfaceHydrologyMixin {
|
||||
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
|
||||
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
|
||||
var generator = (NoiseBasedChunkGenerator) (Object) this;
|
||||
if (WoodlandHydrologyRuntime.enabled(generator)) {
|
||||
if (RiftHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
RiftHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
}));
|
||||
} else if (WoodlandHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
WoodlandHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Alpha.8 retains the upper woodlands and brings mineral pockets into reachable lower layers. */
|
||||
public final class RiftBiomePalette {
|
||||
public enum Kind {
|
||||
OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH,
|
||||
DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS
|
||||
}
|
||||
|
||||
private RiftBiomePalette() {}
|
||||
|
||||
public static Kind select(int blockY, float variation, float moisture) {
|
||||
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 16
|
||||
- Math.clamp(moisture, -1.0F, 1.0F) * 10;
|
||||
// A broad dry mineral province below the inhabited upper layers, with no fluid features.
|
||||
if (elevation < 184 && variation > 0.16F && moisture < 0.35F) return Kind.SULFUR_DEPTHS;
|
||||
// Keep alpha.7's horizontal outcrops; altitude alone never removes the upper forest.
|
||||
if (variation > 0.48F) return Kind.ROCKY_HEATH;
|
||||
if (elevation < 214) {
|
||||
if (variation < -0.45F && moisture > 0.40F) return Kind.BAMBOO_GROVE;
|
||||
double shelteredMoisture = moisture + Math.min(0.55, (214 - elevation) / 160);
|
||||
if (shelteredMoisture > -0.08) return Kind.DARK_GROVE;
|
||||
}
|
||||
if (moisture < -0.55F && variation > -0.25F) return Kind.DRY_WOODLAND;
|
||||
if (Math.abs(variation) < 0.09F && moisture < 0.35F) return Kind.CLEARING;
|
||||
if (variation < -0.33F) return Kind.BIRCH_FOREST;
|
||||
return Kind.OAK_FOREST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.features.TreeFeatures;
|
||||
import net.minecraft.data.worldgen.features.VegetationFeatures;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VegetationBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
|
||||
/** Alpha.8: sheltered vanilla mushroom trees and dark oak on natural lower floors,
|
||||
* plus one globally selected remarkable tree.
|
||||
* Placement runs only in new generation. No world journal or mutable placement counter is retained. */
|
||||
public final class RiftGrovesFeature implements Feature {
|
||||
public static final MapCodec<RiftGrovesFeature> CODEC = MapCodec.unit(RiftGrovesFeature::new);
|
||||
private static final Map<RandomState, Optional<RemarkableTree>> REMARKABLE =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
public record RemarkableTree(BlockPos base, String featureId, int radius, int height) {
|
||||
public RemarkableTree { base = base.immutable(); }
|
||||
}
|
||||
private record Species(ResourceKey<Feature> key, int width, int radius, int height) {}
|
||||
private record Candidate(int x, int z, long rank) {}
|
||||
@FunctionalInterface
|
||||
private interface Protection {
|
||||
boolean intersects(BlockPos base, int radius, int height, int rootDepth);
|
||||
}
|
||||
private static final Species CHERRY = new Species(TreeFeatures.CHERRY, 1, 9, 15);
|
||||
private static final Species[] SPECIES = {CHERRY, CHERRY, CHERRY, CHERRY,
|
||||
new Species(TreeFeatures.SPRUCE, 1, 5, 15), new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14),
|
||||
new Species(TreeFeatures.ACACIA, 1, 7, 14), new Species(TreeFeatures.MANGROVE, 1, 10, 20),
|
||||
new Species(TreeFeatures.PALE_OAK_BONEMEAL, 2, 6, 12)};
|
||||
|
||||
@Override public MapCodec<RiftGrovesFeature> codec() { return CODEC; }
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(ServerLevel level) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)) return Optional.empty();
|
||||
return remarkablePlan(noise, level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) {
|
||||
if (!RiftHydrologyRuntime.enabled(noise)) return Optional.empty();
|
||||
return REMARKABLE.computeIfAbsent(state, ignored -> {
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
RiftHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
|
||||
Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)];
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
for (int x = -152; x <= 152; x += 16) for (int z = -152; z <= 152; z += 16) {
|
||||
if (Math.hypot(x, z) <= 168) candidates.add(new Candidate(x, z,
|
||||
WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x71AFL)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingLong(Candidate::rank).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
for (Candidate candidate : candidates) {
|
||||
int bed = WoodlandGroveGeometry.highestSurface(sampler, candidate.x(), candidate.z());
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, candidate.x(), bed, candidate.z(), species.width())) continue;
|
||||
BlockPos base = new BlockPos(candidate.x(), bed + 1, candidate.z());
|
||||
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))) continue;
|
||||
if (!WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(),
|
||||
species.width(), species.radius(), species.height())) continue;
|
||||
return Optional.of(new RemarkableTree(base, species.key().identifier().toString(), species.radius(), species.height()));
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !RiftHydrologyRuntime.enabled(noise)) return false;
|
||||
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
|
||||
if (Math.abs((long) minX) > 272 || Math.abs((long) minZ) > 272) return false;
|
||||
var state = level.getLevel().getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
RiftHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
|
||||
boolean changed = false;
|
||||
int[] heights = new int[256];
|
||||
Arrays.fill(heights, Integer.MIN_VALUE);
|
||||
var remarkable = remarkablePlan(noise, state);
|
||||
if (remarkable.isPresent()) {
|
||||
var tree = remarkable.get();
|
||||
BlockPos base = tree.base();
|
||||
if ((base.getX() >> 4) == (minX >> 4) && (base.getZ() >> 4) == (minZ >> 4)) {
|
||||
for (Species species : SPECIES) if (species.key().identifier().toString().equals(tree.featureId())) {
|
||||
changed |= tree(level, generator, sampler, state.seed(), base, species, protection, 0x723BL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The scarce tall rooms usually go to dark oak first; one quarter of chunks
|
||||
// give mushroom trees first choice. This changes priority, never attempt budgets
|
||||
// or natural fit requirements, and uses a separate deterministic chunk seed.
|
||||
boolean mushroomsFirst = Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), minX, 0, minZ, 0x8503L), 4) == 0;
|
||||
if (mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Four spaced origins per chunk; the 2x2 trunk and full canopy still have to fit the cave.
|
||||
for (int x = minX + 4; x < minX + 16; x += 8) for (int z = minZ + 4; z < minZ + 16; z += 8) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), placed = 0;
|
||||
for (int bed = highest - 8; bed >= 40; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()) continue;
|
||||
var biome = level.getBiome(base);
|
||||
boolean dark = biome.is(RiftIslandBiomeSource.DARK_GROVE);
|
||||
boolean bamboo = biome.is(RiftIslandBiomeSource.BAMBOO_GROVE);
|
||||
if (!dark && !bamboo) continue;
|
||||
long placementSeed = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x730DL);
|
||||
if (dark) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.DARK_OAK, 2, 6, 12), protection, 0x730DL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 8) == 0) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14), protection, 0x735BL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 3) == 0
|
||||
&& WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1)
|
||||
&& WoodlandGroveGeometry.treeRoom(sampler, x, bed + 1, z, 1, 3, 16)
|
||||
&& !blocked(protection, base, 4, 17) && actualRoom(level, base, 1, 3, 16)) {
|
||||
setBlock(level, base.below(), Blocks.PODZOL.defaultBlockState());
|
||||
changed |= level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(VegetationFeatures.BAMBOO_NO_PODZOL)
|
||||
.value().place(level, generator, RandomSource.create(placementSeed), base);
|
||||
placed++;
|
||||
}
|
||||
if (placed >= 2) break;
|
||||
}
|
||||
}
|
||||
if (!mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Coherent ground patches: about 40% moss, 40% podzol, 20% mycelium.
|
||||
// Small mushrooms are accents (one opportunity per hundred columns), not the canopy.
|
||||
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), floors = 0;
|
||||
long detail = WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x843EL);
|
||||
boolean smallMushroomPlaced = false;
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
|
||||
if (!ground(level, floor) || !level.getBlockState(plant).isAir()) continue;
|
||||
var biome = level.getBiome(plant);
|
||||
if (!biome.is(RiftIslandBiomeSource.DARK_GROVE) && !biome.is(RiftIslandBiomeSource.BAMBOO_GROVE)) continue;
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(protection, plant, 1, 2)) continue;
|
||||
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
|
||||
int soilKind = Math.floorMod(patch, 5);
|
||||
BlockState soil = (soilKind < 2 ? Blocks.MOSS_BLOCK : soilKind < 4 ? Blocks.PODZOL : Blocks.MYCELIUM).defaultBlockState();
|
||||
setBlock(level, floor, soil);
|
||||
if (!smallMushroomPlaced && Math.floorMod(detail, 100) == 0) {
|
||||
BlockState mushroom = (Math.floorMod(detail >>> 8, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
|
||||
if (mushroom.canSurvive(level, plant)) {
|
||||
setBlock(level, plant, mushroom);
|
||||
smallMushroomPlaced = true;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
if (++floors >= 3) break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean mushroomTrees(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler,
|
||||
long seed, Protection protection, int minX, int minZ, int[] heights) {
|
||||
boolean changed = false;
|
||||
RandomSource mushroomSites = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0x8501L));
|
||||
int mushroomAttempts = 1 + mushroomSites.nextInt(3);
|
||||
for (int attempt = 0; attempt < mushroomAttempts; attempt++) {
|
||||
int x = minX + 3 + mushroomSites.nextInt(10), z = minZ + 3 + mushroomSites.nextInt(10);
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights);
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()
|
||||
|| !level.getBiome(base).is(RiftIslandBiomeSource.DARK_GROVE)) continue;
|
||||
long mushroomSeed = WoodlandGroveGeometry.seed(seed, x, bed, z, 0x8502L + attempt);
|
||||
var key = Math.floorMod(mushroomSeed, 2) == 0 ? TreeFeatures.HUGE_BROWN_MUSHROOM : TreeFeatures.HUGE_RED_MUSHROOM;
|
||||
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(key).value();
|
||||
if (!(feature instanceof AbstractHugeMushroomFeature mushroom)) break;
|
||||
int height = mushroom.getTreeHeight(RandomSource.create(mushroomSeed));
|
||||
int radius = mushroom.foliageRadius();
|
||||
if (!mushroomRoom(level, sampler, protection, base, radius, height)) continue;
|
||||
BlockState originalFloor = level.getBlockState(base.below());
|
||||
setBlock(level, base.below(), (Math.floorMod(mushroomSeed, 5) == 0 ? Blocks.MYCELIUM : Blocks.PODZOL).defaultBlockState());
|
||||
// Vanilla consumes its height draw first. Replaying the same random seed
|
||||
// gives precisely the height whose natural envelope was checked above.
|
||||
boolean placed = mushroom.place(level, generator, RandomSource.create(mushroomSeed), base);
|
||||
if (!placed) setBlock(level, base.below(), originalFloor);
|
||||
changed |= placed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
|
||||
BlockPos base, Species species, Protection protection, long salt) {
|
||||
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), species.width())
|
||||
|| !actualRoom(level, base, species.width(), species.radius(), species.height())
|
||||
|| !WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(), species.width(), species.radius(), species.height())) return false;
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
BlockPos floor = base.offset(dx, -1, dz);
|
||||
if (!ground(level, floor)) return false;
|
||||
}
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
setBlock(level, base.offset(dx, -1, dz), Blocks.PODZOL.defaultBlockState());
|
||||
}
|
||||
return level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(species.key()).value()
|
||||
.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, base.getX(), base.getY(), base.getZ(), salt)), base);
|
||||
}
|
||||
|
||||
private static boolean actualRoom(WorldGenLevel level, BlockPos base, int width, int radius, int height) {
|
||||
for (int y = 0; y <= height; y++) {
|
||||
int from = y < 3 ? 0 : -radius, to = y < 3 ? width - 1 : radius;
|
||||
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
|
||||
BlockState current = level.getBlockState(base.offset(dx, y, dz));
|
||||
if (!current.getFluidState().isEmpty() || !(current.isAir() || current.is(BlockTags.LEAVES)
|
||||
|| current.getBlock() instanceof VegetationBlock)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean ground(WorldGenLevel level, BlockPos floor) {
|
||||
BlockState state = level.getBlockState(floor);
|
||||
if (state.is(Blocks.DEEPSLATE)) {
|
||||
// Deep humid ledges can support organic soil. Check every tree foot rather
|
||||
// than admitting this rock globally, including for the remarkable tree.
|
||||
var biome = level.getBiome(floor.above());
|
||||
return biome.is(RiftIslandBiomeSource.DARK_GROVE) || biome.is(RiftIslandBiomeSource.BAMBOO_GROVE);
|
||||
}
|
||||
return state.is(Blocks.STONE) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.PODZOL)
|
||||
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|
||||
|| state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF)
|
||||
|| state.is(Blocks.COBBLESTONE) || state.is(Blocks.MOSSY_COBBLESTONE) || state.is(Blocks.PACKED_MUD);
|
||||
}
|
||||
|
||||
private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
|
||||
int minX, int minZ, int x, int z, int[] heights) {
|
||||
int index = (x - minX) * 16 + z - minZ;
|
||||
if (heights[index] != Integer.MIN_VALUE) return heights[index];
|
||||
// In an untouched column the heightmap bounds the original surface cheaply.
|
||||
// A protected water/lava column may have been carved: recover its raw top
|
||||
// without depending on the current hydrology plan's representation.
|
||||
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
|
||||
if (protection.intersects(new BlockPos(x, level.getMinY(), z), 0, level.getHeight() - 1, 0))
|
||||
ceiling = level.getMaxY();
|
||||
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
|
||||
}
|
||||
|
||||
private static boolean mushroomRoom(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
|
||||
BlockPos base, int radius, int height) {
|
||||
if (blocked(protection, base, radius + 1, height + 1, 4)
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), 1)) return false;
|
||||
for (int depth = 1; depth <= 3; depth++) {
|
||||
BlockPos floor = base.below(depth);
|
||||
if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return false;
|
||||
}
|
||||
// A complete conservative column around the cap handles both brown umbrellas
|
||||
// and the lower rounded red cap. Neither natural rock nor existing logs move.
|
||||
for (int y = 0; y <= height + 1; y++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
|
||||
BlockPos p = base.offset(dx, y, dz);
|
||||
BlockState current = level.getBlockState(p);
|
||||
if (sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 || !current.getFluidState().isEmpty()
|
||||
|| !(current.isAir() || current.is(BlockTags.LEAVES))) return false;
|
||||
}
|
||||
// Being below another surface is not enough: the same column needs an actual
|
||||
// original rock ceiling above the whole mushroom, not an open hillside.
|
||||
for (int y = base.getY() + height + 2; y <= level.getMaxY(); y++)
|
||||
if (sampler.sample(base.getX(), y, base.getZ()) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height) {
|
||||
return blocked(protection, base, radius, height, 4);
|
||||
}
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height, int rootDepth) {
|
||||
return protection.intersects(base, radius, height, rootDepth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
|
||||
/** Alpha.8 river catchments, wider basins and rocky wall springs.
|
||||
* This version is separate from NaturalHydrology so previously created worlds keep their original terrain.
|
||||
* Results retain no chunks, sampler, random state or Minecraft objects. */
|
||||
public final class RiftHydrology {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int MAX_CARVE = 16;
|
||||
public static final int MAX_DEPTH = 5;
|
||||
public static final int MAX_RIVER_BANK_CARVE = 12;
|
||||
private static final int GRID = 8;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
|
||||
|
||||
private RiftHydrology() {}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Sampler { float sample(int x, int y, int z); }
|
||||
|
||||
public enum Kind { POND, LAKE, RIVER }
|
||||
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
|
||||
public record Point(int x, int z) {}
|
||||
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
public record FlowBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
|
||||
public record Spring(long id, Position source, Position outlet, List<Position> flowPath, FlowBounds flowBounds) {
|
||||
public Spring { flowPath = List.copyOf(flowPath); }
|
||||
}
|
||||
|
||||
/** Replaces existing solids bedY-sedimentDepth+1..bedY, with two intact natural supports below.
|
||||
* Water occupies bedY+1..waterY; everything above it through carveTop is removed. For a dry
|
||||
* terrace waterY=-1, and bedY+1..carveTop is removed (at most two original blocks). */
|
||||
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
|
||||
ShoreMaterial material, long featureId, int sedimentDepth) {
|
||||
public boolean hasWater() { return waterY >= 0; }
|
||||
}
|
||||
|
||||
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
|
||||
public Feature { path = List.copyOf(path); }
|
||||
}
|
||||
|
||||
public static final class Plan {
|
||||
private final Map<Long, Cell> cells;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final List<Feature> features;
|
||||
private final List<Spring> springs;
|
||||
private final Map<Long, List<Spring>> springChunks;
|
||||
private final Set<Position> springFlow;
|
||||
private final int sampledColumns;
|
||||
private final long densitySamples;
|
||||
|
||||
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
|
||||
List<Cell> sorted = source.values().stream()
|
||||
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
Map<Long, Cell> ordered = new LinkedHashMap<>();
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : sorted) {
|
||||
ordered.put(key(cell.x(), cell.z()), cell);
|
||||
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
|
||||
}
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
cells = Collections.unmodifiableMap(ordered);
|
||||
chunks = Map.copyOf(byChunk);
|
||||
this.features = List.copyOf(features);
|
||||
this.springs = List.copyOf(springs);
|
||||
this.springFlow = Set.copyOf(springFlow);
|
||||
Map<Long, List<Spring>> groupedSprings = new HashMap<>();
|
||||
for (Spring spring : springs) groupedSprings.computeIfAbsent(
|
||||
key(spring.source().x() >> 4, spring.source().z() >> 4), ignored -> new ArrayList<>()).add(spring);
|
||||
groupedSprings.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
springChunks = Map.copyOf(groupedSprings);
|
||||
sampledColumns = columns;
|
||||
densitySamples = samples;
|
||||
}
|
||||
|
||||
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
|
||||
public Collection<Cell> cells() { return cells.values(); }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Feature> features() { return features; }
|
||||
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
|
||||
public List<Spring> springs() { return springs; }
|
||||
public List<Spring> springsInChunk(int x, int z) { return springChunks.getOrDefault(key(x, z), List.of()); }
|
||||
/** Prediction in undecorated density, not a restriction on Minecraft fluid simulation. */
|
||||
public boolean allowsSpringFlow(int x, int y, int z) { return springFlow.contains(new Position(x, y, z)); }
|
||||
public int sampledColumns() { return sampledColumns; }
|
||||
public long densitySamples() { return densitySamples; }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, Sampler sampler) {
|
||||
return new Planner(seed, sampler).build();
|
||||
}
|
||||
|
||||
private record Column(int top, int solidBottom) {}
|
||||
private record Candidate(int x, int z, double score) {}
|
||||
private record Node(int x, int z, double cost) {}
|
||||
private static final Comparator<Node> NODE_ORDER = Comparator.comparingDouble(Node::cost)
|
||||
.thenComparingInt(Node::x).thenComparingInt(Node::z);
|
||||
|
||||
private static final class Planner {
|
||||
private final long seed;
|
||||
private final Sampler sampler;
|
||||
private final Map<Long, Column> columns = new HashMap<>();
|
||||
private final Map<Long, Integer> coarse = new HashMap<>();
|
||||
private final Map<Long, Cell> cells = new HashMap<>();
|
||||
private final List<Feature> features = new ArrayList<>();
|
||||
private final List<Spring> springs = new ArrayList<>();
|
||||
private final Set<Position> springFlow = new HashSet<>();
|
||||
private long samples;
|
||||
|
||||
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
|
||||
|
||||
Plan build() {
|
||||
List<Candidate> candidates = candidates();
|
||||
addRiver();
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (features.size() >= 4 || attempts++ >= 96) break;
|
||||
Point center = new Point(candidate.x(), candidate.z());
|
||||
if (nearWater(center.x(), center.z(), 38)) continue;
|
||||
Column column = column(center.x(), center.z());
|
||||
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
|
||||
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
|
||||
int target = kind == Kind.LAKE ? 420 + (int) (random(seed, center.x(), center.z()) * 280)
|
||||
: 100 + (int) (random(seed + 1, center.x(), center.z()) * 140);
|
||||
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
|
||||
for (int level : new int[]{column.top() + 1, column.top(), column.top() - 1, column.top() - 2}) {
|
||||
Set<Long> footprint = growBasin(center, level, target);
|
||||
if (footprint == null) continue;
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> wet = waterCells(footprint, level, id);
|
||||
if (wet == null || !contained(wet)) continue;
|
||||
addFeature(id, kind, level, List.of(center), wet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
addRiverSpring();
|
||||
addSedimentSheets();
|
||||
addLowerWallSprings();
|
||||
addSprings();
|
||||
return new Plan(cells, features, springs, springFlow, columns.size(), samples);
|
||||
}
|
||||
|
||||
/** A connected catchment is excavated only in a thick natural plateau. The route is
|
||||
* selected on the actual fissured density, not painted across missing rock. Its two
|
||||
* terminal pools share the same retained waterline; a separate rock spring feeds it. */
|
||||
private void addRiver() {
|
||||
List<Integer> heights = coarse.values().stream().filter(y -> y >= 160).sorted().toList();
|
||||
if (heights.isEmpty()) return;
|
||||
Set<Integer> levels = new java.util.LinkedHashSet<>();
|
||||
for (double quantile : new double[]{.55, .35, .75, .15, .90}) {
|
||||
int top = heights.get(Math.min(heights.size() - 1, (int) (heights.size() * quantile)));
|
||||
for (int cut : new int[]{4, 8, 0}) levels.add(top - cut);
|
||||
}
|
||||
for (int level : levels) {
|
||||
Set<Long> allowed = new HashSet<>();
|
||||
for (int x = -168; x <= 168; x += 4) for (int z = -168; z <= 168; z += 4) {
|
||||
if (!inside(x, z, 170)) continue;
|
||||
boolean safe = riverRock(x, z, level);
|
||||
for (int dx : new int[]{-3, 3}) for (int dz : new int[]{-3, 3}) {
|
||||
if (safe && !riverRock(x + dx, z + dz, level)) safe = false;
|
||||
}
|
||||
if (safe) allowed.add(key(x, z));
|
||||
}
|
||||
for (int attempt = 0; attempt < 4 && allowed.size() > 100; attempt++) {
|
||||
List<Point> route = longestRoute(allowed, level);
|
||||
if (route.size() < 36) break;
|
||||
Point first = route.getFirst(), last = route.getLast();
|
||||
if (Math.hypot(last.x() - first.x(), last.z() - first.z()) < 100) break;
|
||||
List<Point> path = smoothRoute(route);
|
||||
Set<Long> wet = new HashSet<>();
|
||||
Set<Long> bad = new HashSet<>();
|
||||
for (Point at : path) {
|
||||
int radius = field(at.x(), at.z(), 23, 8119) > .52 ? 3 : 2;
|
||||
for (int dx = -radius - 1; dx <= radius + 1; dx++) for (int dz = -radius - 1; dz <= radius + 1; dz++) {
|
||||
if (!riverRock(at.x() + dx, at.z() + dz, level)) {
|
||||
for (int gx = -1; gx <= 1; gx++) for (int gz = -1; gz <= 1; gz++)
|
||||
bad.add(key(Math.floorDiv(at.x(), 4) * 4 + gx * 4, Math.floorDiv(at.z(), 4) * 4 + gz * 4));
|
||||
}
|
||||
if (dx * dx + dz * dz <= radius * radius + 1) wet.add(key(at.x() + dx, at.z() + dz));
|
||||
}
|
||||
}
|
||||
if (!bad.isEmpty()) { allowed.removeAll(bad); continue; }
|
||||
// Both end pools are required and remain connected to the channel.
|
||||
boolean pools = true;
|
||||
for (Point endpoint : List.of(first, last)) {
|
||||
Set<Long> pool = growRiverPool(endpoint, level);
|
||||
if (pool.size() < 90) { pools = false; break; }
|
||||
wet.addAll(pool);
|
||||
}
|
||||
if (!pools) { allowed.remove(key(first.x(), first.z())); allowed.remove(key(last.x(), last.z())); continue; }
|
||||
long id = featureId(features.size());
|
||||
Map<Long, Cell> river = new HashMap<>();
|
||||
boolean safe = true;
|
||||
for (long k : wet) {
|
||||
Point at = point(k);
|
||||
Column rock = column(at.x(), at.z());
|
||||
int depth = 2;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !wet.contains(key(at.x() + d[0], at.z() + d[1]));
|
||||
if (edge) depth = 1;
|
||||
int bed = level - depth;
|
||||
if (rock.top() < bed || rock.top() - bed > MAX_CARVE || rock.solidBottom() > bed - 6) { safe = false; break; }
|
||||
river.put(k, new Cell(at.x(), at.z(), level, bed, Math.max(level, rock.top()),
|
||||
wetMaterial(at.x(), at.z(), edge), id, 3));
|
||||
}
|
||||
if (!safe || !contained(river)) break;
|
||||
addFeature(id, Kind.RIVER, level, path, river);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Long> growRiverPool(Point center, int level) {
|
||||
Set<Long> result = new HashSet<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
Set<Long> seen = new HashSet<>();
|
||||
queue.add(new Node(center.x(), center.z(), 0));
|
||||
while (!queue.isEmpty() && result.size() < 240) {
|
||||
Node at = queue.remove();
|
||||
if (!seen.add(key(at.x(), at.z())) || Math.hypot(at.x() - center.x(), at.z() - center.z()) > 12
|
||||
|| !riverRock(at.x(), at.z(), level)) continue;
|
||||
boolean retained = true;
|
||||
for (int[] d : CARDINALS) retained &= solid(at.x() + d[0], level, at.z() + d[1]);
|
||||
if (!retained) continue;
|
||||
result.add(key(at.x(), at.z()));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = at.x() + d[0], z = at.z() + d[1];
|
||||
queue.add(new Node(x, z, Math.hypot(x - center.x(), z - center.z())
|
||||
+ field(x, z, 8, 411) * 6 + Math.max(0, column(x,z).top() - level) * .4));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean riverRock(int x, int z, int level) {
|
||||
Column rock = column(x, z);
|
||||
return rock.top() >= level && rock.top() <= level + 12 && rock.solidBottom() <= level - 8;
|
||||
}
|
||||
|
||||
private List<Point> longestRoute(Set<Long> allowed, int level) {
|
||||
Set<Long> remaining = new HashSet<>(allowed);
|
||||
List<Point> longest = List.of();
|
||||
while (!remaining.isEmpty()) {
|
||||
long start = remaining.stream().min(Long::compare).orElseThrow();
|
||||
Map<Long, Long> first = routeTree(start, allowed);
|
||||
remaining.removeAll(first.keySet());
|
||||
if (first.size() < 36) continue;
|
||||
long far = lastKey(first);
|
||||
Map<Long, Long> second = routeTree(far, first.keySet());
|
||||
long end = lastKey(second);
|
||||
Map<Long, Long> shaped = riverRouteTree(far, end, first.keySet(), level);
|
||||
List<Point> path = new ArrayList<>();
|
||||
for (long at = end; ; at = shaped.get(at)) {
|
||||
path.add(point(at));
|
||||
if (at == far) break;
|
||||
}
|
||||
if (path.size() > longest.size()) longest = path;
|
||||
}
|
||||
return longest;
|
||||
}
|
||||
|
||||
private Map<Long, Long> riverRouteTree(long start, long end, Set<Long> allowed, int level) {
|
||||
Map<Long, Long> parent = new HashMap<>();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
Point origin = point(start);
|
||||
parent.put(start, start); costs.put(start, 0.0); queue.add(new Node(origin.x(), origin.z(), 0));
|
||||
while (!queue.isEmpty()) {
|
||||
Node at = queue.remove();
|
||||
long current = key(at.x(), at.z());
|
||||
if (at.cost() > costs.getOrDefault(current, Double.POSITIVE_INFINITY)) continue;
|
||||
if (current == end) break;
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = at.x() + d[0] * 4, z = at.z() + d[1] * 4;
|
||||
long next = key(x, z);
|
||||
if (!allowed.contains(next)) continue;
|
||||
double cost = at.cost() + Math.hypot(d[0], d[1]) * (1 + Math.max(0, column(x,z).top() - level) * .14
|
||||
+ field(x, z, 27, 8113) * 1.8);
|
||||
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||
parent.put(next, current); costs.put(next, cost); queue.add(new Node(x,z,cost));
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
private List<Point> smoothRoute(List<Point> coarsePath) {
|
||||
record Smooth(double x, double z) {}
|
||||
List<Smooth> values = coarsePath.stream().map(p -> new Smooth(p.x(), p.z())).toList();
|
||||
// Bend over several grid steps rather than rounding each voxel-sized corner.
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
List<Smooth> next = new ArrayList<>();
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
if (i == 0 || i == values.size() - 1) { next.add(values.get(i)); continue; }
|
||||
double x = 0, z = 0, sum = 0;
|
||||
for (int offset = -4; offset <= 4; offset++) {
|
||||
int weight = 5 - Math.abs(offset);
|
||||
Smooth p = values.get(Math.clamp(i + offset, 0, values.size() - 1));
|
||||
x += p.x() * weight; z += p.z() * weight; sum += weight;
|
||||
}
|
||||
next.add(new Smooth(x / sum, z / sum));
|
||||
}
|
||||
values = next;
|
||||
}
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
List<Smooth> next = new ArrayList<>(); next.add(values.getFirst());
|
||||
for (int i = 1; i < values.size(); i++) {
|
||||
Smooth a = values.get(i - 1), b = values.get(i);
|
||||
next.add(new Smooth(a.x() * .75 + b.x() * .25, a.z() * .75 + b.z() * .25));
|
||||
next.add(new Smooth(a.x() * .25 + b.x() * .75, a.z() * .25 + b.z() * .75));
|
||||
}
|
||||
next.add(values.getLast()); values = next;
|
||||
}
|
||||
List<Point> path = new ArrayList<>();
|
||||
for (int i = 1; i < values.size(); i++) {
|
||||
Smooth a = values.get(i - 1), b = values.get(i);
|
||||
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(b.x() - a.x()), Math.abs(b.z() - a.z()))));
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double t = step / (double) steps;
|
||||
Point p = new Point((int) Math.round(a.x() + (b.x() - a.x()) * t), (int) Math.round(a.z() + (b.z() - a.z()) * t));
|
||||
if (path.isEmpty() || !path.getLast().equals(p)) path.add(p);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private Map<Long, Long> routeTree(long start, Set<Long> allowed) {
|
||||
Map<Long, Long> parent = new LinkedHashMap<>();
|
||||
ArrayDeque<Long> queue = new ArrayDeque<>();
|
||||
parent.put(start, start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
long current = queue.removeFirst();
|
||||
Point at = point(current);
|
||||
for (int[] d : NEIGHBORS) {
|
||||
long next = key(at.x() + d[0] * 4, at.z() + d[1] * 4);
|
||||
if (allowed.contains(next) && !parent.containsKey(next)) { parent.put(next, current); queue.addLast(next); }
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
private long lastKey(Map<Long, Long> values) {
|
||||
long result = 0;
|
||||
for (long key : values.keySet()) result = key;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A real spring emerges from one of the river's taller rock banks and drops into the
|
||||
* connected retained channel. The source replaces rock; its mouth is the carved valley. */
|
||||
private void addRiverSpring() {
|
||||
Feature river = features.stream().filter(f -> f.kind() == Kind.RIVER).findFirst().orElse(null);
|
||||
if (river == null) return;
|
||||
List<Cell> bank = cells.values().stream().filter(c -> c.featureId() == river.id())
|
||||
.sorted(Comparator.comparingInt(Cell::carveTop).reversed().thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
|
||||
for (Cell wet : bank) for (int[] d : CARDINALS) {
|
||||
int x = wet.x() + d[0], z = wet.z() + d[1];
|
||||
if (cells.containsKey(key(x, z))) continue;
|
||||
for (int height : new int[]{8, 7, 6, 5}) {
|
||||
int y = wet.waterY() + height;
|
||||
if (wet.carveTop() < y) continue;
|
||||
Position source = new Position(x, y, z), outlet = new Position(wet.x(), y, wet.z());
|
||||
boolean safe = true;
|
||||
for (int sy = y - 3; sy <= y + 1; sy++) safe &= flowSolid(new Position(x, sy, z), outlet);
|
||||
for (int[] side : CARDINALS) {
|
||||
if (side[0] == -d[0] && side[1] == -d[1]) continue;
|
||||
safe &= flowSolid(new Position(x + side[0], y, z + side[1]), outlet);
|
||||
}
|
||||
if (!safe) continue;
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.volume().stream().noneMatch(p -> {
|
||||
Cell c = cells.get(key(p.x(), p.z()));
|
||||
return c != null && c.featureId() == river.id() && p.y() <= c.waterY() && p.y() > c.bedY();
|
||||
})) continue;
|
||||
long id = mix(seed ^ 0xA81BE2L);
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Candidate> candidates() {
|
||||
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
|
||||
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
|
||||
}
|
||||
}
|
||||
List<Candidate> result = new ArrayList<>();
|
||||
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
|
||||
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
|
||||
int height = coarse.getOrDefault(key(x, z), -1);
|
||||
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
|
||||
double average = 0;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
|
||||
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
|
||||
average += neighbor * 0.25;
|
||||
}
|
||||
if (safe) result.add(new Candidate(x, z,
|
||||
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
return result;
|
||||
}
|
||||
|
||||
private int coarseTop(int x, int z) {
|
||||
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Column column(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Column old = columns.get(key);
|
||||
if (old != null) return old;
|
||||
int top = -1;
|
||||
// Exact scan, including everything above the planned water: a cave roof cannot be
|
||||
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
|
||||
for (int y = 383; y >= 24; y--) {
|
||||
if (solid(x, y, z)) { top = y; break; }
|
||||
}
|
||||
int bottom = top;
|
||||
if (top >= 0) {
|
||||
while (bottom > Math.max(16, top - 40) && solid(x, bottom - 1, z)) bottom--;
|
||||
}
|
||||
Column created = new Column(top, bottom);
|
||||
columns.put(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private boolean eligible(int x, int z, int level) {
|
||||
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
|
||||
Cell old = cells.get(key(x, z));
|
||||
if (old != null && old.hasWater() && old.waterY() != level) return false;
|
||||
Column column = column(x, z);
|
||||
return column.top() >= level - 3 && column.top() < level + 6
|
||||
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 4;
|
||||
}
|
||||
|
||||
private Set<Long> growBasin(Point center, int level, int target) {
|
||||
if (!eligible(center.x(), center.z(), level)) return null;
|
||||
Set<Long> wet = new HashSet<>();
|
||||
Map<Long, Double> costs = new HashMap<>();
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
|
||||
queue.add(new Node(center.x(), center.z(), 0));
|
||||
costs.put(key(center.x(), center.z()), 0.0);
|
||||
while (!queue.isEmpty() && wet.size() < target) {
|
||||
Node current = queue.remove();
|
||||
if (current.cost() > costs.getOrDefault(key(current.x(), current.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
if (!eligible(current.x(), current.z(), level)) continue;
|
||||
wet.add(key(current.x(), current.z()));
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = current.x() + d[0], z = current.z() + d[1];
|
||||
if (Math.hypot(x - center.x(), z - center.z()) > 40 || wet.contains(key(x, z))) continue;
|
||||
if (!eligible(x, z, level)) continue;
|
||||
double step = Math.hypot(d[0], d[1]);
|
||||
double cost = current.cost() + step * (1 + Math.max(0, column(x, z).top() - level) * 0.8
|
||||
+ terrainNoise(x, z) * 4.5 + field(x + 71, z - 93, 21, 107) * 3.5);
|
||||
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(key(x, z), cost);
|
||||
queue.add(new Node(x, z, cost));
|
||||
}
|
||||
}
|
||||
wet = connectedTo(wet, key(center.x(), center.z()));
|
||||
if (wet.size() < Math.min(target, 30)) return null;
|
||||
// Filling a hollow requires every lower connected outlet to be included. Abort rather
|
||||
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
|
||||
List<Long> work = new ArrayList<>(wet);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = point.x() + d[0], z = point.z() + d[1];
|
||||
long neighbor = key(x, z);
|
||||
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
|
||||
if (wet.size() >= 1200 || Math.hypot(x - center.x(), z - center.z()) > 48
|
||||
|| !eligible(x, z, level)) return null;
|
||||
wet.add(neighbor);
|
||||
work.add(neighbor);
|
||||
}
|
||||
}
|
||||
return wet;
|
||||
}
|
||||
|
||||
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
|
||||
Map<Long, Cell> result = new HashMap<>();
|
||||
for (long key : footprint) {
|
||||
Point p = point(key);
|
||||
if (!eligible(p.x(), p.z(), level)) return null;
|
||||
boolean edge = false;
|
||||
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
|
||||
Column column = column(p.x(), p.z());
|
||||
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
|
||||
depth = Math.min(depth, 8 - Math.max(0, column.top() - level));
|
||||
if (depth < 1 || depth > MAX_DEPTH) return null;
|
||||
int bed = level - depth;
|
||||
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
|
||||
ShoreMaterial material = wetMaterial(p.x(), p.z(), edge);
|
||||
int sedimentDepth = sedimentDepth(p.x(), p.z());
|
||||
if (column.solidBottom() > bed - sedimentDepth - 1) return null;
|
||||
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()),
|
||||
material, id, sedimentDepth));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean contained(Map<Long, Cell> wet) {
|
||||
for (Cell cell : wet.values()) {
|
||||
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
|
||||
if (!solid(cell.x(), y, cell.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
int x = cell.x() + d[0], z = cell.z() + d[1];
|
||||
Cell neighbor = wet.get(key(x, z));
|
||||
if (neighbor == null) neighbor = cells.get(key(x, z));
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
if (neighbor != null && neighbor.hasWater()) {
|
||||
if (neighbor.waterY() != cell.waterY()) return false;
|
||||
if (y > neighbor.bedY()) continue;
|
||||
}
|
||||
if (!solid(x, y, z)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
|
||||
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
|
||||
int count = 0;
|
||||
for (Cell cell : wet.values()) {
|
||||
Cell old = cells.get(key(cell.x(), cell.z()));
|
||||
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
|
||||
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
|
||||
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
|
||||
}
|
||||
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
|
||||
}
|
||||
|
||||
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
|
||||
Set<Long> connected = new HashSet<>();
|
||||
List<Long> work = new ArrayList<>();
|
||||
connected.add(start); work.add(start);
|
||||
for (int i = 0; i < work.size(); i++) {
|
||||
Point point = point(work.get(i));
|
||||
for (int[] d : CARDINALS) {
|
||||
long next = key(point.x() + d[0], point.z() + d[1]);
|
||||
if (footprint.contains(next) && connected.add(next)) work.add(next);
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
private record ShoreNode(int x, int z, int level, long feature, double cost) {}
|
||||
|
||||
/** Sediment follows shallow topography in broad, discontinuous sheets. A second nearby
|
||||
* pocket can share the same sheet; the result is not a one-block material ring. */
|
||||
private void addSedimentSheets() {
|
||||
Comparator<ShoreNode> order = Comparator.comparingDouble(ShoreNode::cost)
|
||||
.thenComparingInt(ShoreNode::x).thenComparingInt(ShoreNode::z).thenComparingLong(ShoreNode::feature);
|
||||
Set<Long> rivers = features.stream().filter(f -> f.kind() == Kind.RIVER).map(Feature::id)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
PriorityQueue<ShoreNode> queue = new PriorityQueue<>(order);
|
||||
Map<Long, Double> best = new HashMap<>();
|
||||
for (Cell cell : cells.values()) if (cell.hasWater()) {
|
||||
queue.add(new ShoreNode(cell.x(), cell.z(), cell.waterY(), cell.featureId(), 0));
|
||||
best.put(key(cell.x(), cell.z()), 0.0);
|
||||
}
|
||||
while (!queue.isEmpty()) {
|
||||
ShoreNode node = queue.remove();
|
||||
if (node.cost() > best.getOrDefault(key(node.x(), node.z()), Double.POSITIVE_INFINITY)) continue;
|
||||
boolean riverBank = rivers.contains(node.feature());
|
||||
Column column = column(node.x(), node.z());
|
||||
Cell existing = cells.get(key(node.x(), node.z()));
|
||||
if (existing == null) {
|
||||
boolean springRock = springs.stream().anyMatch(spring ->
|
||||
Math.abs(spring.source().x() - node.x()) <= 4 && Math.abs(spring.source().z() - node.z()) <= 4);
|
||||
if (springRock) continue;
|
||||
double extent = riverBank ? 18 + field(node.x(), node.z(), 25, 131) * 6
|
||||
: 9 + field(node.x(), node.z(), 25, 131) * 10;
|
||||
if (node.cost() > extent) continue;
|
||||
int delta = column.top() - node.level();
|
||||
if (delta < 0 || delta > (riverBank ? 14 : 5)) continue;
|
||||
// Low shore: waterline, then one-block steps. Cut at most two existing blocks;
|
||||
// far deposits keep the original landform and feather back into grass/stone.
|
||||
int terrace = node.level() + (int) Math.floor(node.cost() / (riverBank ? 2.5 : 4.5));
|
||||
int cut = riverBank ? MAX_RIVER_BANK_CARVE : 2;
|
||||
int bed = Math.max(node.level(), Math.max(column.top() - cut, Math.min(column.top(), terrace)));
|
||||
int depth = sedimentDepth(node.x(), node.z());
|
||||
if (column.solidBottom() > bed - depth - 1) continue;
|
||||
boolean safe = true;
|
||||
for (int[] d : CARDINALS) {
|
||||
Cell next = cells.get(key(node.x() + d[0], node.z() + d[1]));
|
||||
if (next != null && next.hasWater() && bed < next.waterY()) safe = false;
|
||||
}
|
||||
if (!safe) continue;
|
||||
ShoreMaterial material = dryMaterial(node.x(), node.z(), node.cost() / extent);
|
||||
cells.put(key(node.x(), node.z()), new Cell(node.x(), node.z(), -1, bed,
|
||||
column.top(), material, node.feature(), depth));
|
||||
}
|
||||
for (int[] d : NEIGHBORS) {
|
||||
int x = node.x() + d[0], z = node.z() + d[1];
|
||||
if (!inside(x, z, RADIUS - 2)) continue;
|
||||
Cell water = cells.get(key(x, z));
|
||||
if (water != null && water.hasWater()) continue;
|
||||
Column next = column(x, z);
|
||||
if (next.top() < node.level() || next.top() > node.level() + (riverBank ? 14 : 5)) continue;
|
||||
double cost = node.cost() + Math.hypot(d[0], d[1])
|
||||
* (0.7 + Math.abs(next.top() - column.top()) * 0.32 + field(x, z, 12, 211) * 0.6);
|
||||
if (cost > (riverBank ? 24 : 19) || cost >= best.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
|
||||
best.put(key(x, z), cost);
|
||||
queue.add(new ShoreNode(x, z, node.level(), node.feature(), cost));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int sedimentDepth(int x, int z) { return 3 + (int) (field(x, z, 15, 319) * 2.999); }
|
||||
|
||||
private ShoreMaterial wetMaterial(int x, int z, boolean edge) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
if (geology < 0.46) return ShoreMaterial.STONE;
|
||||
if (geology < 0.65) return ShoreMaterial.GRAVEL;
|
||||
if (!edge && geology > 0.78) return ShoreMaterial.CLAY;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private ShoreMaterial dryMaterial(int x, int z, double distance) {
|
||||
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
|
||||
double grass = field(x + 103, z - 137, 19, 521);
|
||||
if (grass > 0.62 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
|
||||
if (geology < 0.46) return ShoreMaterial.STONE;
|
||||
if (geology < 0.65) return ShoreMaterial.GRAVEL;
|
||||
return ShoreMaterial.SAND;
|
||||
}
|
||||
|
||||
private record Cliff(int x, int z, int dx, int dz, double score) {}
|
||||
private record FlowNode(Position position, int reach) {}
|
||||
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
|
||||
|
||||
/** Pick rare exposed rock niches at the edges of real upper surfaces. The source replaces
|
||||
* one rock block under an existing roof. Its outlet is already air over an eight-block drop;
|
||||
* no source, shelf, dam, rock column or ocean is ever placed in empty terrain. */
|
||||
private void addSprings() {
|
||||
List<Cliff> cliffs = new ArrayList<>();
|
||||
for (int x = -264; x <= 264; x += GRID) for (int z = -264; z <= 264; z += GRID) {
|
||||
if (!inside(x, z, 264)) continue;
|
||||
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top < 88) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
int nx = x + d[0] * GRID, nz = z + d[1] * GRID;
|
||||
int lower = coarse.computeIfAbsent(key(nx, nz), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
if (top - lower < 16) continue;
|
||||
cliffs.add(new Cliff(x, z, d[0], d[1],
|
||||
Math.min(64, top - lower) * 0.06 + random(seed + 811, x + d[0], z + d[1]) * 3));
|
||||
}
|
||||
}
|
||||
cliffs.sort(Comparator.comparingDouble(Cliff::score).reversed()
|
||||
.thenComparingInt(Cliff::x).thenComparingInt(Cliff::z)
|
||||
.thenComparingInt(Cliff::dx).thenComparingInt(Cliff::dz));
|
||||
int attempts = 0;
|
||||
for (Cliff cliff : cliffs) {
|
||||
if (springs.size() >= 3 || attempts++ >= 160) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2, -4, 4}) {
|
||||
for (int along = 0; along < GRID; along++) {
|
||||
int x = cliff.x() + cliff.dx() * along - cliff.dz() * tangent;
|
||||
int z = cliff.z() + cliff.dz() * along + cliff.dx() * tangent;
|
||||
if (!inside(x, z, 266)) continue;
|
||||
int ox = x + cliff.dx(), oz = z + cliff.dz();
|
||||
Column rock = column(x, z), outletColumn = column(ox, oz);
|
||||
if (rock.top() < 80 || rock.top() - outletColumn.top() < 14) continue;
|
||||
// Five blocks below the highest natural surface is below the soil cap.
|
||||
int floor = Math.max(64, Math.max(rock.top() - 20, outletColumn.top() + 8));
|
||||
for (int y = rock.top() - 5; y >= floor; y -= 3) {
|
||||
Position source = new Position(x, y, z), outlet = new Position(ox, y, oz);
|
||||
if (!separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz()) || nearModifiedGround(source, 32)) continue;
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > y - 12) continue;
|
||||
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean nearModifiedGround(Position source, int distance) {
|
||||
for (Cell cell : cells.values()) {
|
||||
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0),
|
||||
source.y() - cell.carveTop());
|
||||
long dx = source.x() - cell.x(), dz = source.z() - cell.z();
|
||||
if (dx * dx + dz * dz + (long) dy * dy < (long) distance * distance) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean separatedSpring(Position source) {
|
||||
for (Spring spring : springs) {
|
||||
double horizontal = Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z());
|
||||
int dy = Math.abs(source.y() - spring.source().y());
|
||||
if (Math.hypot(horizontal, dy) < 64 || dy < 12) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private record Wall(int x, int y, int z, int dx, int dz, double score) {}
|
||||
|
||||
/** Probe a few exposed faces below the highest surface as well. This finds overhangs and
|
||||
* lower strata that a highest-column heightmap cannot represent, without inventing a ledge. */
|
||||
private void addLowerWallSprings() {
|
||||
List<Wall> candidates = new ArrayList<>();
|
||||
for (int x = -240; x <= 240; x += 16) for (int z = -240; z <= 240; z += 16) {
|
||||
if (!inside(x, z, 240)) continue;
|
||||
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
|
||||
for (int y = top - 32; y >= 64; y -= 16) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int[] direction : CARDINALS) {
|
||||
if (solid(x + direction[0] * 4, y, z + direction[1] * 4)) continue;
|
||||
candidates.add(new Wall(x, y, z, direction[0], direction[1],
|
||||
random(seed + 1217 + y, x + direction[0], z + direction[1]) * 3
|
||||
+ Math.min(100, top - y) * 0.015));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Wall::score).reversed()
|
||||
.thenComparingInt(Wall::x).thenComparingInt(Wall::z).thenComparingInt(Wall::y)
|
||||
.thenComparingInt(Wall::dx).thenComparingInt(Wall::dz));
|
||||
int attempts = 0;
|
||||
for (Wall candidate : candidates) {
|
||||
if (springs.size() >= 2 || attempts++ >= 96) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, 3, -3, 6, -6}) {
|
||||
int y = candidate.y() + dy;
|
||||
if (y < 56 || column(x, z).top() - y < 24) continue;
|
||||
Position source = new Position(x, y, z);
|
||||
if (!separatedSpring(source) || !springNiche(source, candidate.dx(), candidate.dz())
|
||||
|| nearModifiedGround(source, 32)) continue;
|
||||
Position outlet = new Position(x + candidate.dx(), y, z + candidate.dz());
|
||||
Flow flow = traceSpring(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > y - 12) continue;
|
||||
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
|
||||
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
|
||||
springFlow.addAll(flow.volume());
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean springNiche(Position source, int dx, int dz) {
|
||||
for (int y = source.y() - 3; y <= source.y() + 1; y++) {
|
||||
if (!solid(source.x(), y, source.z())) return false;
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == dx && d[1] == dz) continue;
|
||||
if (!solid(source.x() + d[0], source.y(), source.z() + d[1])) return false;
|
||||
}
|
||||
for (int y = source.y(); y >= source.y() - 8; y--) {
|
||||
if (solid(source.x() + dx, y, source.z() + dz)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Predict drainage through undecorated rock to select springs and inspection bounds.
|
||||
* Descend through air first, then explore seven horizontal steps on a shelf. Later trees
|
||||
* can divert the actual fluid: this prediction is not a bound on decorated-world physics.
|
||||
* Integration tests trace actual water back to its source within the inspected chunk halo. */
|
||||
private Flow traceSpring(Position source, Position outlet) {
|
||||
Map<Position, Integer> reach = new HashMap<>();
|
||||
Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
|
||||
reach.put(source, 7);
|
||||
queue.add(new FlowNode(source, 7));
|
||||
Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
FlowNode node = queue.removeFirst();
|
||||
Position at = node.position();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 24000) return null;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (!flowSolid(below, source)) {
|
||||
enqueueFlow(below, 7, at, queue, reach, parent);
|
||||
continue;
|
||||
}
|
||||
if (node.reach() <= 0) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (flowSolid(next, source)) continue;
|
||||
enqueueFlow(next, node.reach() - 1, at, queue, reach, parent);
|
||||
}
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY;
|
||||
int minZ = source.z(), maxZ = minZ;
|
||||
for (Position position : reach.keySet()) {
|
||||
minX = Math.min(minX, position.x()); maxX = Math.max(maxX, position.x());
|
||||
minY = Math.min(minY, position.y()); maxY = Math.max(maxY, position.y());
|
||||
minZ = Math.min(minZ, position.z()); maxZ = Math.max(maxZ, position.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Flow(Set.copyOf(reach.keySet()), path, new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Position source) {
|
||||
if (at.equals(source)) return false;
|
||||
Cell cell = cells.get(key(at.x(), at.z()));
|
||||
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
return solid(at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static void enqueueFlow(Position next, int available, Position from, ArrayDeque<FlowNode> queue,
|
||||
Map<Position, Integer> reach, Map<Position, Position> parent) {
|
||||
if (available <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, available);
|
||||
queue.addLast(new FlowNode(next, available));
|
||||
}
|
||||
|
||||
private boolean nearWater(int x, int z, int distance) {
|
||||
for (Feature feature : features) {
|
||||
Point point = feature.path().getFirst();
|
||||
if (Math.hypot(x - point.x(), z - point.z()) < distance) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { samples++; return sampler.sample(x, y, z) > 0; }
|
||||
private long featureId(int index) { return mix(seed ^ (0x51A7E7L + index * 0x9E3779B97F4A7C15L)); }
|
||||
|
||||
private double terrainNoise(int x, int z) { return field(x, z, 8, 31); }
|
||||
|
||||
private double field(int x, int z, int scale, long salt) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
|
||||
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double a = random(seed + salt, gx, gz) * (1 - tx) + random(seed + salt, gx + 1, gz) * tx;
|
||||
double b = random(seed + salt, gx, gz + 1) * (1 - tx) + random(seed + salt, gx + 1, gz + 1) * tx;
|
||||
return a * (1 - tz) + b * tz;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return Math.hypot((double) x, z) < radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static Point point(long key) { return new Point((int) (key >> 32), (int) key); }
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Alpha.8 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
|
||||
public final class RiftHydrologyRuntime {
|
||||
private static final Map<RandomState, RiftHydrology.Plan> PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private static final Map<RandomState, RiftLavaDeposit.Plan> LAVA_PLANS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private RiftHydrologyRuntime() {}
|
||||
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) {
|
||||
return generator.generatorSettings().is(SanctuarySpawn.RIFT_SETTINGS);
|
||||
}
|
||||
|
||||
public static RiftHydrology.Plan riftPlan(ServerLevel level) {
|
||||
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static RiftHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
if (!enabled(generator)) throw new IllegalArgumentException("Rift hydrology is not enabled for this generator");
|
||||
return PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var plan = RiftHydrology.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
|
||||
SanctuaryMod.LOGGER.info("Sanctuary rift hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
|
||||
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
|
||||
(System.nanoTime() - start) / 1_000_000);
|
||||
SanctuaryMod.LOGGER.info("Sanctuary rift catchments: {}", plan.features().stream().map(f -> f.kind() + " Y" + f.waterY() + " water=" + f.waterCells() + " path=" + f.path().size() + " " + f.bounds()).toList());
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
|
||||
public static RiftLavaDeposit.Plan lavaPlan(ServerLevel level) {
|
||||
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
|
||||
level.getChunkSource().randomState());
|
||||
}
|
||||
|
||||
public static RiftLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) {
|
||||
// Acquire the water plan before the lava cache lock; the water planner never acquires it.
|
||||
var water = plan(generator, randomState);
|
||||
return LAVA_PLANS.computeIfAbsent(randomState, state -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var lava = RiftLavaDeposit.create(state.seed(),
|
||||
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water);
|
||||
SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms",
|
||||
state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000);
|
||||
return lava;
|
||||
});
|
||||
}
|
||||
|
||||
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
|
||||
if (!enabled(generator)) return;
|
||||
var chunkPos = chunk.getPos();
|
||||
if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L
|
||||
|| Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return;
|
||||
var plan = plan(generator, randomState);
|
||||
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var outlets = new java.util.ArrayList<>(plan.springs().stream().map(RiftHydrology.Spring::outlet)
|
||||
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList());
|
||||
var lava = lavaPlan(generator, randomState);
|
||||
for (var fall : lava.falls()) {
|
||||
var outlet = fall.outlet();
|
||||
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z())
|
||||
outlets.add(new RiftHydrology.Position(outlet.x(), outlet.y(), outlet.z()));
|
||||
}
|
||||
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
|
||||
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
|
||||
if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
// Validate all replacements and two intact support layers before modifying this chunk.
|
||||
for (var cell : cells) {
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
|
||||
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
|
||||
}
|
||||
for (var support : lavaSupports) {
|
||||
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
|
||||
if (chunk.getBlockState(pos).ignitedByLava()) {
|
||||
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
|
||||
}
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
|
||||
}
|
||||
for (var cell : cells) {
|
||||
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
|
||||
pos.set(cell.x(), cell.bedY() - depth, cell.z());
|
||||
BlockState material = RiftMaterials.sedimentBlock(cell, randomState.seed(), depth);
|
||||
chunk.setBlockState(pos, material, 0);
|
||||
}
|
||||
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
|
||||
pos.set(cell.x(), y, cell.z());
|
||||
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
|
||||
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
|
||||
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
}
|
||||
for (var spring : springs) {
|
||||
var source = spring.source();
|
||||
pos.set(source.x(), source.y(), source.z());
|
||||
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
|
||||
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
for (var cell : lavaCells) {
|
||||
pos.set(cell.x(), cell.y(), cell.z());
|
||||
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
|
||||
if (cell.lava()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
// This existing vanilla queue is consumed after neighbouring chunks finish decoration.
|
||||
// Mark the outlet in its own chunk, including when its source belongs to a neighbour.
|
||||
for (var outlet : outlets) {
|
||||
pos.set(outlet.x(), outlet.y(), outlet.z());
|
||||
chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
}
|
||||
|
||||
/** Reserve water, sediment and hot-rock volumes before placing vegetation. */
|
||||
public static boolean protects(NoiseBasedChunkGenerator generator, RandomState randomState, BlockPos base,
|
||||
int radius, int height, int rootDepth) {
|
||||
var water = plan(generator, randomState);
|
||||
var lava = lavaPlan(generator, randomState);
|
||||
int low = base.getY() - rootDepth, high = base.getY() + height;
|
||||
for (int x = base.getX() - radius; x <= base.getX() + radius; x++) {
|
||||
for (int z = base.getZ() - radius; z <= base.getZ() + radius; z++) {
|
||||
var cell = water.cellAt(x, z);
|
||||
if (cell != null && low <= cell.carveTop() && high >= cell.bedY() - cell.sedimentDepth() - 1) return true;
|
||||
}
|
||||
}
|
||||
for (var spring : water.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
if (base.getX() + radius >= bounds.minX() - 2 && base.getX() - radius <= bounds.maxX() + 2
|
||||
&& base.getZ() + radius >= bounds.minZ() - 2 && base.getZ() - radius <= bounds.maxZ() + 2
|
||||
&& low <= bounds.maxY() + 2 && high >= bounds.minY() - 2) return true;
|
||||
var source = spring.source();
|
||||
if (Math.abs(base.getX() - source.x()) <= radius + 3 && Math.abs(base.getZ() - source.z()) <= radius + 3
|
||||
&& low <= source.y() + 3 && high >= source.y() - 3) return true;
|
||||
}
|
||||
for (var bounds : lava.flowBounds()) {
|
||||
if (base.getX() + radius >= bounds.minX() - 8 && base.getX() - radius <= bounds.maxX() + 8
|
||||
&& base.getZ() + radius >= bounds.minZ() - 8 && base.getZ() - radius <= bounds.maxZ() + 8
|
||||
&& low <= bounds.maxY() + 8 && high >= bounds.minY() - 8) return true;
|
||||
}
|
||||
for (var cell : lava.cells()) if (Math.abs(base.getX() - cell.x()) <= radius + 2 && Math.abs(base.getZ() - cell.z()) <= radius + 2
|
||||
&& low <= cell.y() + 2 && high >= cell.y() - 2) return true;
|
||||
for (var support : lava.supports()) if (Math.abs(base.getX() - support.x()) <= radius + 2 && Math.abs(base.getZ() - support.z()) <= radius + 2
|
||||
&& low <= support.y() + 2 && high >= support.y() - 2) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
|
||||
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
|
||||
throw new IllegalStateException("Rift hydrology differs from terrain at " + pos + " for seed " + seed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** Seeded alpha.8 rift climate, separate from all saved earlier biome-source codecs. */
|
||||
public final class RiftIslandBiomeSource extends BiomeSource {
|
||||
public static final ResourceKey<Biome> OAK_FOREST = key("rift_oak_forest");
|
||||
public static final ResourceKey<Biome> BIRCH_FOREST = key("rift_birch_forest");
|
||||
public static final ResourceKey<Biome> CLEARING = key("rift_clearing");
|
||||
public static final ResourceKey<Biome> DRY_WOODLAND = key("rift_dry_woodland");
|
||||
public static final ResourceKey<Biome> ROCKY_HEATH = key("rift_rocky_heath");
|
||||
public static final ResourceKey<Biome> DARK_GROVE = key("rift_dark_grove");
|
||||
public static final ResourceKey<Biome> BAMBOO_GROVE = key("rift_bamboo_grove");
|
||||
public static final ResourceKey<Biome> SULFUR_DEPTHS = key("rift_sulfur_depths");
|
||||
|
||||
public static final MapCodec<RiftIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.CODEC.fieldOf("oak_forest").forGetter(source -> source.oakForest),
|
||||
Biome.CODEC.fieldOf("birch_forest").forGetter(source -> source.birchForest),
|
||||
Biome.CODEC.fieldOf("clearing").forGetter(source -> source.clearing),
|
||||
Biome.CODEC.fieldOf("dry_woodland").forGetter(source -> source.dryWoodland),
|
||||
Biome.CODEC.fieldOf("rocky_heath").forGetter(source -> source.rockyHeath),
|
||||
Biome.CODEC.fieldOf("dark_grove").forGetter(source -> source.darkGrove),
|
||||
Biome.CODEC.fieldOf("bamboo_grove").forGetter(source -> source.bambooGrove),
|
||||
Biome.CODEC.fieldOf("sulfur_depths").forGetter(source -> source.sulfurDepths)
|
||||
).apply(instance, RiftIslandBiomeSource::new));
|
||||
|
||||
private final Holder<Biome> oakForest;
|
||||
private final Holder<Biome> birchForest;
|
||||
private final Holder<Biome> clearing;
|
||||
private final Holder<Biome> dryWoodland;
|
||||
private final Holder<Biome> rockyHeath;
|
||||
private final Holder<Biome> darkGrove;
|
||||
private final Holder<Biome> bambooGrove;
|
||||
private final Holder<Biome> sulfurDepths;
|
||||
|
||||
public RiftIslandBiomeSource(Holder<Biome> oakForest, Holder<Biome> birchForest, Holder<Biome> clearing,
|
||||
Holder<Biome> dryWoodland, Holder<Biome> rockyHeath, Holder<Biome> darkGrove,
|
||||
Holder<Biome> bambooGrove, Holder<Biome> sulfurDepths) {
|
||||
this.oakForest = oakForest;
|
||||
this.birchForest = birchForest;
|
||||
this.clearing = clearing;
|
||||
this.dryWoodland = dryWoodland;
|
||||
this.rockyHeath = rockyHeath;
|
||||
this.darkGrove = darkGrove;
|
||||
this.bambooGrove = bambooGrove;
|
||||
this.sulfurDepths = sulfurDepths;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<RiftIslandBiomeSource> codec() { return CODEC; }
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.of(oakForest, birchForest, clearing, dryWoodland, rockyHeath, darkGrove, bambooGrove, sulfurDepths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return (quartX, quartY, quartZ) -> {
|
||||
int x = quartX * 4, y = quartY * 4, z = quartZ * 4;
|
||||
float variation = sampler.temperature().sampleValue(x, 0, z);
|
||||
float moisture = sampler.humidity().sampleValue(x, 0, z);
|
||||
return switch (RiftBiomePalette.select(y, variation, moisture)) {
|
||||
case OAK_FOREST -> oakForest;
|
||||
case BIRCH_FOREST -> birchForest;
|
||||
case CLEARING -> clearing;
|
||||
case DRY_WOODLAND -> dryWoodland;
|
||||
case ROCKY_HEATH -> rockyHeath;
|
||||
case DARK_GROVE -> darkGrove;
|
||||
case BAMBOO_GROVE -> bambooGrove;
|
||||
case SULFUR_DEPTHS -> sulfurDepths;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private static ResourceKey<Biome> key(String path) {
|
||||
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityBuffer;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Alpha.8 wrapper. The existing raised-island density and all earlier world settings stay intact. */
|
||||
public record RiftIslandDensity(DensityFunction terrain, DensityFunction warp, DensityFunction detail) implements DensityFunction {
|
||||
public static final Identifier LAYOUT_RANDOM = SanctuaryMod.id("rift_layout_alpha8");
|
||||
public static final MapCodec<RiftIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(RiftIslandDensity::terrain),
|
||||
DensityFunction.CODEC.fieldOf("warp").forGetter(RiftIslandDensity::warp),
|
||||
DensityFunction.CODEC.fieldOf("detail").forGetter(RiftIslandDensity::detail)
|
||||
).apply(instance, RiftIslandDensity::new));
|
||||
|
||||
@Override
|
||||
public DensitySampler compileSampler(CompileContext context) {
|
||||
return new RiftSampler(terrain.compileSampler(context), warp.compileSampler(context), detail.compileSampler(context),
|
||||
RiftShape.layout(context.createRandom(LAYOUT_RANDOM).nextLong()));
|
||||
}
|
||||
|
||||
/** Diagnostics read exactly the immutable layout used by the compiled field. */
|
||||
public RiftShape.Layout layout(RandomState state) {
|
||||
DensitySampler sampler = state.getSampler(this);
|
||||
if (sampler instanceof RiftSampler rift) return rift.layout();
|
||||
throw new IllegalStateException("The rift density was unexpectedly replaced by another sampler");
|
||||
}
|
||||
|
||||
public static final class RiftSampler implements DensitySampler {
|
||||
private final DensitySampler terrain, warp, detail;
|
||||
private final RiftShape.Layout layout;
|
||||
|
||||
private RiftSampler(DensitySampler terrain, DensitySampler warp, DensitySampler detail, RiftShape.Layout layout) {
|
||||
this.terrain = terrain; this.warp = warp; this.detail = detail; this.layout = layout;
|
||||
}
|
||||
public RiftShape.Layout layout() { return layout; }
|
||||
|
||||
@Override
|
||||
public float sampleValue(SamplerContext context, int x, int y, int z) {
|
||||
float natural = terrain.sampleValue(context, x, y, z);
|
||||
if (natural <= 0 || !RiftShape.mayAffect(layout, x, z)) return natural;
|
||||
return RiftShape.density(layout, x, y, z, natural,
|
||||
warp.sampleValue(context, x, y, z), detail.sampleValue(context, x, y, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sampleVolume(SamplerContext context, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(context, buffer, volume, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new RiftIslandDensity(rule.rewrite(terrain), rule.rewrite(warp), rule.rewrite(detail));
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(Math.min(-1.0F, terrain.range().min()), terrain.range().max()); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<RiftIslandDensity> codec() { return CODEC; }
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** Alpha.8 lava: an optional covered pocket and up to two deep rock outlets.
|
||||
* All sources replace natural rock; Minecraft alone produces the falling lava. */
|
||||
public final class RiftLavaDeposit {
|
||||
public static final int RADIUS = 192;
|
||||
public static final int WATER_CLEARANCE = 32;
|
||||
public static final int LAVA_VOLUME = 18;
|
||||
private static final int ENTRY_DISTANCE = 8;
|
||||
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
|
||||
.thenComparingInt(Position::z).thenComparingInt(Position::y);
|
||||
private static final Comparator<Cell> CELL_ORDER = Comparator.comparingInt(Cell::x)
|
||||
.thenComparingInt(Cell::z).thenComparingInt(Cell::y);
|
||||
|
||||
private RiftLavaDeposit() {}
|
||||
|
||||
public record Position(int x, int y, int z) {}
|
||||
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
|
||||
public record Cell(int x, int y, int z, boolean lava) {}
|
||||
|
||||
public static final class Plan {
|
||||
private final List<Cell> cells;
|
||||
private final List<Cell> lavaCells;
|
||||
private final List<Position> supports;
|
||||
private final Optional<Position> access;
|
||||
private final Map<Long, List<Cell>> chunks;
|
||||
private final Map<Long, List<Position>> supportChunks;
|
||||
private final List<RiftLavaFlows.Fall> falls;
|
||||
private final Map<Long, List<RiftLavaFlows.Fall>> fallChunks;
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
|
||||
this(cells, supports, access, List.of());
|
||||
}
|
||||
|
||||
private Plan(List<Cell> cells, Set<Position> supports, Position access, List<RiftLavaFlows.Fall> falls) {
|
||||
this.falls = List.copyOf(falls);
|
||||
Map<Long, List<RiftLavaFlows.Fall>> groupedFalls = new HashMap<>();
|
||||
for (var fall : falls) groupedFalls.computeIfAbsent(
|
||||
key(fall.source().x() >> 4, fall.source().z() >> 4), ignored -> new ArrayList<>()).add(fall);
|
||||
groupedFalls.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
fallChunks = Map.copyOf(groupedFalls);
|
||||
this.cells = cells.stream().sorted(CELL_ORDER).toList();
|
||||
lavaCells = this.cells.stream().filter(Cell::lava).toList();
|
||||
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
|
||||
this.access = Optional.ofNullable(access);
|
||||
Map<Long, List<Cell>> byChunk = new HashMap<>();
|
||||
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(cell);
|
||||
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
chunks = Map.copyOf(byChunk);
|
||||
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
|
||||
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(support);
|
||||
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
|
||||
supportChunks = Map.copyOf(bySupportChunk);
|
||||
}
|
||||
|
||||
public List<Cell> cells() { return cells; }
|
||||
public List<Cell> lavaCells() { return lavaCells; }
|
||||
public List<Position> supports() { return supports; }
|
||||
public List<RiftLavaFlows.Fall> falls() { return falls; }
|
||||
public List<RiftLavaFlows.Fall> fallsInChunk(int x, int z) { return fallChunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> flowSources() { return falls.stream().map(RiftLavaFlows.Fall::source).toList(); }
|
||||
public List<RiftHydrology.FlowBounds> flowBounds() { return falls.stream().map(RiftLavaFlows.Fall::flowBounds).toList(); }
|
||||
/** Player feet on a natural ledge, with three blocks of headroom below a roof or sky. */
|
||||
public Optional<Position> access() { return access; }
|
||||
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
|
||||
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
|
||||
}
|
||||
|
||||
public static Plan create(long seed, RiftHydrology.Sampler sampler, RiftHydrology.Plan hydrology) {
|
||||
Plan pocket = new Search(seed, sampler, hydrology).find();
|
||||
List<RiftLavaFlows.Fall> falls = RiftLavaFlows.create(seed, sampler, hydrology, pocket);
|
||||
List<Cell> cells = new ArrayList<>(pocket.cells());
|
||||
Set<Position> supports = new HashSet<>(pocket.supports());
|
||||
for (var fall : falls) {
|
||||
Position source = fall.source();
|
||||
cells.add(new Cell(source.x(), source.y(), source.z(), true));
|
||||
supports.addAll(fall.supports());
|
||||
}
|
||||
return new Plan(cells, supports, pocket.access().orElse(null), falls);
|
||||
}
|
||||
|
||||
private record Candidate(int x, int z, int dx, int dz, double score) {}
|
||||
private record LowerCandidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final RiftHydrology.Sampler sampler;
|
||||
private final RiftHydrology.Plan hydrology;
|
||||
private final Map<Long, Integer> heights = new HashMap<>();
|
||||
|
||||
Search(long seed, RiftHydrology.Sampler sampler, RiftHydrology.Plan hydrology) {
|
||||
this.seed = seed;
|
||||
this.sampler = sampler;
|
||||
this.hydrology = hydrology;
|
||||
}
|
||||
|
||||
Plan find() {
|
||||
Plan lower = findLowerPocket();
|
||||
if (lower != null) return lower;
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
int ledge = top(x, z);
|
||||
if (ledge < 64 || ledge > 300) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
|
||||
int rise = top(cx, cz) - ledge;
|
||||
if (rise < 12) continue;
|
||||
candidates.add(new Candidate(x, z, direction[0], direction[1],
|
||||
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (attempts++ >= 192) break;
|
||||
// Refine the coarse edge, including a ledge lying between two eight-block samples.
|
||||
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
return new Plan(List.of(), Set.of(), null);
|
||||
}
|
||||
|
||||
/** Prefer a real lower ledge below an overhang. The chamber uses exactly the same rock
|
||||
* shell as the outdoor fallback; only the entrance may now have a natural ceiling. */
|
||||
private Plan findLowerPocket() {
|
||||
List<LowerCandidate> candidates = new ArrayList<>();
|
||||
for (int x = -176; x <= 176; x += 16) for (int z = -176; z <= 176; z += 16) {
|
||||
if (!inside(x, z, RADIUS - 8)) continue;
|
||||
for (int coarseY = 64; coarseY <= 160; coarseY += 8) {
|
||||
if (!solid(x, coarseY, z) || solid(x, coarseY + 8, z)) continue;
|
||||
int level = coarseY;
|
||||
for (int y = coarseY + 7; y > coarseY; y--) {
|
||||
if (solid(x, y, z)) { level = y; break; }
|
||||
}
|
||||
if (level > 160 || top(x, z) < level + 12) continue;
|
||||
for (int[] direction : DIRECTIONS) {
|
||||
int inwardX = x - direction[0] * 16, inwardZ = z - direction[1] * 16;
|
||||
if (!inside(inwardX, inwardZ, RADIUS - 12)
|
||||
|| !solid(inwardX, level + 2, inwardZ) || !solid(inwardX, level + 10, inwardZ)) continue;
|
||||
candidates.add(new LowerCandidate(x, level, z, direction[0], direction[1],
|
||||
random(seed + level + 0x108E2L, x - direction[0], z - direction[1])
|
||||
+ (160 - level) * .002));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(LowerCandidate::score).reversed()
|
||||
.thenComparingInt(LowerCandidate::x).thenComparingInt(LowerCandidate::z)
|
||||
.thenComparingInt(LowerCandidate::y).thenComparingInt(LowerCandidate::dx).thenComparingInt(LowerCandidate::dz));
|
||||
int attempts = 0;
|
||||
for (LowerCandidate candidate : candidates) {
|
||||
if (attempts++ >= 128) break;
|
||||
for (int along = 0; along < 16; along++) for (int tangent : new int[]{0, -3, 3}) {
|
||||
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int level = Math.min(160, candidate.y() + 5); level >= Math.max(64, candidate.y() - 5); level--) {
|
||||
// Reject solid cave walls and missing floors before expensive shell probes.
|
||||
if (!solid(x, level, z) || solid(x, level + 1, z)) continue;
|
||||
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz(), level, true);
|
||||
if (plan != null) return plan;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
|
||||
return tryNiche(entryX, entryZ, dx, dz, top(entryX, entryZ), false);
|
||||
}
|
||||
|
||||
private Plan tryNiche(int entryX, int entryZ, int dx, int dz, int level, boolean underground) {
|
||||
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
|
||||
if (!inside(cx, cz, RADIUS - 12) || level < 64 || level > (underground ? 160 : 300)
|
||||
|| top(cx, cz) < level + 12) return null;
|
||||
// A lower entrance has genuine overhead island rock, but does not need a sky view.
|
||||
if (underground && top(entryX, entryZ) < level + 12) return null;
|
||||
int airLimit = underground ? level + 3 : 383;
|
||||
for (int y = level + 1; y <= airLimit; y++) if (solid(entryX, y, entryZ)) return null;
|
||||
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
|
||||
if (underground ? !clearOfWater(cx, level, cz) : !clearOfWater(cx, cz)) return null;
|
||||
|
||||
List<Cell> cells = new ArrayList<>();
|
||||
Set<Position> changed = new HashSet<>();
|
||||
Set<Position> supports = new HashSet<>();
|
||||
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
|
||||
for (int y = level - 1; y <= level + 2; y++) {
|
||||
if (!solid(x, y, z)) return null;
|
||||
add(cells, changed, x, y, z, y <= level);
|
||||
}
|
||||
}
|
||||
// A dry lip separates the ledge from the pool. Only the air above it is opened.
|
||||
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level + 1; y <= level + 2; y++) {
|
||||
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
|
||||
}
|
||||
}
|
||||
// Every carved column must lie under eight continuous natural rock blocks. This
|
||||
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
|
||||
Map<Long, Integer> highest = new HashMap<>();
|
||||
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
|
||||
for (var column : highest.entrySet()) {
|
||||
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
|
||||
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
|
||||
// surround prevent escape through an adjacent cave. No part of this shell is created.
|
||||
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
|
||||
for (int y = level - 4; y <= level + 2; y++) {
|
||||
if (changed.contains(new Position(x, y, z))) continue;
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
// The natural roof also covers a conservative envelope around local lava fire spread.
|
||||
// Its thickness keeps later surface vegetation well above the lava chamber.
|
||||
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
|
||||
for (int y = level + 3; y <= level + 10; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
|
||||
int x = cx + dx * distance, z = cz + dz * distance;
|
||||
for (int y = level - 2; y <= level; y++) {
|
||||
if (!requireRock(supports, x, y, z)) return null;
|
||||
}
|
||||
}
|
||||
supports.removeAll(changed);
|
||||
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
|
||||
}
|
||||
|
||||
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
|
||||
if (!solid(x, y, z)) return false;
|
||||
supports.add(new Position(x, y, z));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int z) {
|
||||
// The extra ten blocks include both the entrance and the complete protected shell.
|
||||
int clearance = WATER_CLEARANCE + 10;
|
||||
for (var cell : hydrology.cells()) {
|
||||
long dx = x - cell.x(), dz = z - cell.z();
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
var bounds = spring.flowBounds();
|
||||
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
|
||||
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
|
||||
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearOfWater(int x, int level, int z) {
|
||||
// This box covers chamber, dry approach, intact shell and roof. A lake far above is
|
||||
// allowed; an actual waterfall crossing this level still excludes the entire pocket.
|
||||
var pocket = new RiftHydrology.FlowBounds(x - 10, level - 4, z - 10, x + 10, level + 10, z + 10);
|
||||
for (var cell : hydrology.cells()) {
|
||||
var ground = new RiftHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
|
||||
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
|
||||
if (distanceSquared(pocket, ground) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spring : hydrology.springs()) {
|
||||
if (distanceSquared(pocket, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static long distanceSquared(RiftHydrology.FlowBounds a, RiftHydrology.FlowBounds b) {
|
||||
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
|
||||
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
|
||||
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private int top(int x, int z) {
|
||||
return heights.computeIfAbsent(key(x, z), ignored -> {
|
||||
for (int y = 383; y >= 32; y -= 4) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
|
||||
return y;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
|
||||
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
|
||||
}
|
||||
|
||||
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
|
||||
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
|
||||
private static double random(long seed, int x, int z) {
|
||||
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.RiftLavaDeposit.Position;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Selects a few volcanic seeps in deep exposed rock. Sources replace rock; their outlets and
|
||||
* entire falling columns are left empty for vanilla lava ticks. No retaining wall is generated. */
|
||||
public final class RiftLavaFlows {
|
||||
public static final int MIN_SOURCE_Y = 40;
|
||||
public static final int MAX_SOURCE_Y = 160;
|
||||
public static final int MAX_FALLS = 2;
|
||||
public static final int HORIZONTAL_REACH = 3;
|
||||
public static final int FIRE_CLEARANCE = 8;
|
||||
public static final int WATER_CLEARANCE = 24;
|
||||
private static final int RADIUS = 224;
|
||||
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
|
||||
.thenComparingInt(Position::z).thenComparingInt(Position::y);
|
||||
|
||||
private RiftLavaFlows() {}
|
||||
|
||||
/** flowBounds predicts undecorated rock drainage; it does not override Minecraft physics.
|
||||
* supports contains only unchanged natural rock around the source, never the source itself. */
|
||||
public record Fall(long id, Position source, Position outlet, List<Position> flowPath,
|
||||
RiftHydrology.FlowBounds flowBounds, List<Position> supports) {
|
||||
public Fall { flowPath = List.copyOf(flowPath); supports = List.copyOf(supports); }
|
||||
}
|
||||
|
||||
public static List<Fall> create(long seed, RiftHydrology.Sampler sampler,
|
||||
RiftHydrology.Plan water, RiftLavaDeposit.Plan pocket) {
|
||||
return new Search(seed, sampler, water, pocket).find();
|
||||
}
|
||||
|
||||
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
|
||||
private record Node(Position at, int reach) {}
|
||||
private record Trace(List<Position> path, RiftHydrology.FlowBounds bounds) {}
|
||||
|
||||
private static final class Search {
|
||||
private final long seed;
|
||||
private final RiftHydrology.Sampler sampler;
|
||||
private final RiftHydrology.Plan water;
|
||||
private final RiftLavaDeposit.Plan pocket;
|
||||
private final List<Fall> falls = new ArrayList<>();
|
||||
|
||||
Search(long seed, RiftHydrology.Sampler sampler, RiftHydrology.Plan water, RiftLavaDeposit.Plan pocket) {
|
||||
this.seed = seed; this.sampler = sampler; this.water = water; this.pocket = pocket;
|
||||
}
|
||||
|
||||
List<Fall> find() {
|
||||
List<Candidate> candidates = new ArrayList<>();
|
||||
// Coarse probes below the surface can see a lower overhang that a heightmap hides.
|
||||
for (int x = -216; x <= 216; x += 8) for (int z = -216; z <= 216; z += 8) {
|
||||
if ((long) x * x + (long) z * z >= (long) RADIUS * RADIUS) continue;
|
||||
for (int y = MIN_SOURCE_Y; y <= MAX_SOURCE_Y; y += 8) {
|
||||
if (!solid(x, y, z)) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
if (solid(x + d[0] * 4, y, z + d[1] * 4)) continue;
|
||||
double score = random(seed + y, x + d[0], z + d[1])
|
||||
+ (MAX_SOURCE_Y - y) * .002;
|
||||
candidates.add(new Candidate(x, y, z, d[0], d[1], score));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
|
||||
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
|
||||
.thenComparingInt(Candidate::y).thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
|
||||
int attempts = 0;
|
||||
for (Candidate candidate : candidates) {
|
||||
if (falls.size() >= MAX_FALLS || attempts++ >= 160) break;
|
||||
boolean placed = false;
|
||||
for (int tangent : new int[]{0, -2, 2}) {
|
||||
for (int along = 0; along < 4; along++) {
|
||||
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
|
||||
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
|
||||
for (int dy : new int[]{0, 2, -2, 4, -4}) {
|
||||
Position source = new Position(x, candidate.y() + dy, z);
|
||||
if (source.y() < MIN_SOURCE_Y || source.y() > MAX_SOURCE_Y || !separated(source)) continue;
|
||||
List<Position> supports = niche(source, candidate.dx(), candidate.dz());
|
||||
if (supports == null) continue;
|
||||
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
|
||||
Trace flow = trace(source, outlet);
|
||||
if (flow == null || flow.bounds().minY() > source.y() - 12 || !clearOfWater(flow.bounds())) continue;
|
||||
long id = mix(seed ^ (0x1A7AFA11L + falls.size() * 0x9E3779B97F4A7C15L));
|
||||
falls.add(new Fall(id, source, outlet, flow.path(), flow.bounds(), supports));
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
if (placed) break;
|
||||
}
|
||||
}
|
||||
return List.copyOf(falls);
|
||||
}
|
||||
|
||||
private boolean separated(Position source) {
|
||||
RiftHydrology.FlowBounds point = bounds(source);
|
||||
if (!clearOfWater(point)) return false;
|
||||
for (Fall fall : falls) {
|
||||
if (distanceSquared(source, fall.source()) < 80L * 80
|
||||
|| distanceSquared(point, fall.flowBounds()) < 40L * 40) return false;
|
||||
}
|
||||
for (var cell : pocket.cells()) {
|
||||
if (distanceSquared(source, new Position(cell.x(), cell.y(), cell.z())) < 32L * 32) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Position> niche(Position source, int dx, int dz) {
|
||||
if (!solid(source)) return null;
|
||||
Set<Position> supports = new HashSet<>();
|
||||
// The source sits in rock, with a short intact roof, floor and three back/side faces.
|
||||
// The spill column needs no floor: its absence is precisely what makes a cascade.
|
||||
for (int dy = -3; dy <= 8; dy++) {
|
||||
if (dy == 0) continue;
|
||||
Position at = new Position(source.x(), source.y() + dy, source.z());
|
||||
if (!solid(at)) return null;
|
||||
supports.add(at);
|
||||
}
|
||||
for (int[] d : CARDINALS) {
|
||||
if (d[0] == dx && d[1] == dz) continue;
|
||||
for (int depth = 1; depth <= 2; depth++) {
|
||||
Position at = new Position(source.x() + d[0] * depth, source.y(), source.z() + d[1] * depth);
|
||||
if (!solid(at)) return null;
|
||||
supports.add(at);
|
||||
}
|
||||
}
|
||||
for (int dy = 0; dy >= -8; dy--) {
|
||||
if (solid(source.x() + dx, source.y() + dy, source.z() + dz)) return null;
|
||||
}
|
||||
return supports.stream().sorted(POSITION_ORDER).toList();
|
||||
}
|
||||
|
||||
private boolean clearOfWater(RiftHydrology.FlowBounds bounds) {
|
||||
for (var cell : water.cells()) {
|
||||
var column = new RiftHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
|
||||
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
|
||||
if (distanceSquared(bounds, column) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
for (var spring : water.springs()) {
|
||||
if (distanceSquared(bounds, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Descending lava resets its horizontal reach to three in this non-ultrawarm dimension.
|
||||
* Explore all downhill branches through rock, so selection does not depend on chunk order. */
|
||||
private Trace trace(Position source, Position outlet) {
|
||||
Map<Position, Integer> reach = new HashMap<>();
|
||||
Map<Position, Position> parent = new HashMap<>();
|
||||
ArrayDeque<Node> queue = new ArrayDeque<>();
|
||||
reach.put(source, HORIZONTAL_REACH);
|
||||
queue.addLast(new Node(source, HORIZONTAL_REACH));
|
||||
Position lowest = source;
|
||||
while (!queue.isEmpty()) {
|
||||
Node node = queue.removeFirst();
|
||||
Position at = node.at();
|
||||
if (node.reach() < reach.getOrDefault(at, -1)) continue;
|
||||
if (at.y() < lowest.y()) lowest = at;
|
||||
if (reach.size() > 12000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
|
||||
if (at.y() <= 0) continue;
|
||||
Position below = new Position(at.x(), at.y() - 1, at.z());
|
||||
if (!flowSolid(below, source)) {
|
||||
enqueue(below, HORIZONTAL_REACH, at, reach, parent, queue);
|
||||
continue;
|
||||
}
|
||||
if (node.reach() <= 0) continue;
|
||||
for (int[] d : CARDINALS) {
|
||||
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
|
||||
if (!flowSolid(next, source)) enqueue(next, node.reach() - 1, at, reach, parent, queue);
|
||||
}
|
||||
}
|
||||
if (!reach.containsKey(outlet)) return null;
|
||||
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
|
||||
for (Position at : reach.keySet()) {
|
||||
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x());
|
||||
minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
|
||||
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
|
||||
}
|
||||
List<Position> path = new ArrayList<>();
|
||||
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
|
||||
Collections.reverse(path);
|
||||
return new Trace(List.copyOf(path), new RiftHydrology.FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
|
||||
}
|
||||
|
||||
private boolean flowSolid(Position at, Position source) {
|
||||
if (at.equals(source)) return false;
|
||||
var cell = water.cellAt(at.x(), at.z());
|
||||
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
|
||||
return solid(at);
|
||||
}
|
||||
|
||||
private boolean solid(Position at) { return solid(at.x(), at.y(), at.z()); }
|
||||
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
|
||||
}
|
||||
|
||||
private static void enqueue(Position next, int available, Position from, Map<Position, Integer> reach,
|
||||
Map<Position, Position> parent, ArrayDeque<Node> queue) {
|
||||
if (available <= reach.getOrDefault(next, -1)) return;
|
||||
if (!reach.containsKey(next)) parent.put(next, from);
|
||||
reach.put(next, available);
|
||||
queue.addLast(new Node(next, available));
|
||||
}
|
||||
|
||||
private static RiftHydrology.FlowBounds bounds(Position at) {
|
||||
return new RiftHydrology.FlowBounds(at.x(), at.y(), at.z(), at.x(), at.y(), at.z());
|
||||
}
|
||||
|
||||
private static long distanceSquared(Position a, Position b) {
|
||||
long dx = a.x() - b.x(), dy = a.y() - b.y(), dz = a.z() - b.z();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static long distanceSquared(RiftHydrology.FlowBounds a, RiftHydrology.FlowBounds b) {
|
||||
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
|
||||
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
|
||||
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private static double random(long seed, int x, int z) {
|
||||
return (mix(seed ^ ((long) x << 32) ^ (z & 0xffffffffL)) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Alpha.8 shore deposits: broad continuous patches through the existing sediment thickness. */
|
||||
public final class RiftMaterials {
|
||||
private RiftMaterials() {}
|
||||
|
||||
public static BlockState sedimentBlock(RiftHydrology.Cell cell, long seed, int depth) {
|
||||
if (cell.material() == RiftHydrology.ShoreMaterial.GRASS) {
|
||||
return (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
|
||||
}
|
||||
double patch = patch(seed, cell.x(), cell.z(), 32);
|
||||
Block material;
|
||||
if (cell.hasWater() && patch < 0.24) material = Blocks.CLAY;
|
||||
else if (patch < 0.53) material = Blocks.SAND;
|
||||
else if (patch < 0.68) material = Blocks.GRAVEL;
|
||||
else {
|
||||
double rock = patch(seed ^ 0x4c6179657273L, cell.x(), cell.z(), 40);
|
||||
material = rock < 0.28 ? Blocks.GRANITE : rock < 0.45 ? Blocks.DIORITE
|
||||
: rock < 0.65 ? Blocks.ANDESITE : Blocks.STONE;
|
||||
}
|
||||
return material.defaultBlockState();
|
||||
}
|
||||
|
||||
private static double patch(long seed, int x, int z, int scale) {
|
||||
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
|
||||
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
|
||||
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
|
||||
double near = lerp(value(seed, gx, gz), value(seed, gx + 1, gz), tx);
|
||||
double far = lerp(value(seed, gx, gz + 1), value(seed, gx + 1, gz + 1), tx);
|
||||
return lerp(near, far, tz);
|
||||
}
|
||||
|
||||
private static double value(long seed, int x, int z) {
|
||||
return (WoodlandGroveGeometry.seed(seed, x, 0, z, 0x53484f5245L) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
private static double smooth(double t) { return t * t * (3 - 2 * t); }
|
||||
private static double lerp(double a, double b, double t) { return a + (b - a) * t; }
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Alpha.8 fracture geometry. All operations remove material from the existing island field. */
|
||||
public final class RiftShape {
|
||||
public static final int INNER_UNTOUCHED_RADIUS = 20;
|
||||
public static final int OUTER_UNTOUCHED_RADIUS = 224;
|
||||
private static final double TWO_PI = Math.PI * 2.0;
|
||||
|
||||
public record Point(double x, double z) {}
|
||||
public record Segment(double x, double z, double dx, double dz, double halfLength,
|
||||
double halfWidth, double bend, double phase) {}
|
||||
public record Rift(long id, Segment main, List<Segment> branches) {
|
||||
public Rift { branches = List.copyOf(branches); }
|
||||
}
|
||||
public record Layout(List<Rift> rifts) {
|
||||
public Layout { rifts = List.copyOf(rifts); }
|
||||
}
|
||||
|
||||
private RiftShape() {}
|
||||
|
||||
/** Seed is obtained from Minecraft's named compile-context random stream. */
|
||||
public static Layout layout(long seed) {
|
||||
List<Rift> rifts = new ArrayList<>();
|
||||
int count = 1 + (int) Math.floorMod(mix(seed + 1), 3);
|
||||
double rotation = unit(seed + 2) * TWO_PI;
|
||||
for (int i = 0; i < count; i++) {
|
||||
long salt = mix(seed + 0x51F71L + i * 0x9e3779b97f4a7c15L);
|
||||
double bearing = rotation + i * TWO_PI / count + (unit(salt) - 0.5) * 0.42;
|
||||
double radius = 76 + unit(salt + 1) * 60;
|
||||
// Tangential placement leaves most coast and the small central neighbourhood intact.
|
||||
// This protects existing material; it does not create a spawn island or solid core.
|
||||
double x = Math.rint(Math.cos(bearing) * radius), z = Math.rint(Math.sin(bearing) * radius);
|
||||
double angle = bearing + Math.PI / 2 + (unit(salt + 2) - 0.5) * 0.72;
|
||||
double length = 84 + unit(salt + 3) * 76;
|
||||
Segment main = new Segment(x, z, Math.cos(angle), Math.sin(angle), length / 2,
|
||||
2.8 + unit(salt + 4) * 1.7, 6 + unit(salt + 5) * 7, unit(salt + 6) * TWO_PI);
|
||||
List<Segment> branches = new ArrayList<>();
|
||||
int branchCount = 1 + (int) Math.floorMod(mix(salt + 7), 2);
|
||||
for (int branch = 0; branch < branchCount; branch++) {
|
||||
double along = (branch == 0 ? -0.25 : 0.27) * length;
|
||||
Point anchor = centerline(main, along);
|
||||
double branchAngle = angle + (branch == 0 ? -1 : 1) * (0.40 + unit(salt + 8 + branch) * 0.35);
|
||||
double halfLength = 12 + unit(salt + 12 + branch) * 13;
|
||||
double dx = Math.cos(branchAngle), dz = Math.sin(branchAngle);
|
||||
Segment raw = new Segment(anchor.x() + dx * halfLength, anchor.z() + dz * halfLength,
|
||||
dx, dz, halfLength, 2.2 + unit(salt + 16 + branch) * 0.7,
|
||||
2 + unit(salt + 20 + branch) * 3, unit(salt + 24 + branch) * TWO_PI);
|
||||
// Translate the curved start back onto the parent; no isolated artificial shafts.
|
||||
Point start = centerline(raw, -halfLength);
|
||||
branches.add(new Segment(raw.x() + anchor.x() - start.x(), raw.z() + anchor.z() - start.z(),
|
||||
dx, dz, halfLength, raw.halfWidth(), raw.bend(), raw.phase()));
|
||||
}
|
||||
rifts.add(new Rift(salt, main, branches));
|
||||
}
|
||||
return new Layout(rifts);
|
||||
}
|
||||
|
||||
public static Point centerline(Segment segment, double along) {
|
||||
double bend = curve(segment, Math.clamp(along, -segment.halfLength(), segment.halfLength()));
|
||||
return new Point(segment.x() + segment.dx() * along - segment.dz() * bend,
|
||||
segment.z() + segment.dz() * along + segment.dx() * bend);
|
||||
}
|
||||
|
||||
/** A cheap conservative bound avoids sampling two extra noises throughout the whole island. */
|
||||
public static boolean mayAffect(Layout layout, int x, int z) {
|
||||
double radius = Math.hypot((double) x, z);
|
||||
if (radius <= INNER_UNTOUCHED_RADIUS || radius >= OUTER_UNTOUCHED_RADIUS) return false;
|
||||
for (Rift rift : layout.rifts()) {
|
||||
if (near(rift.main(), x, z)) return true;
|
||||
for (Segment branch : rift.branches()) if (near(branch, x, z)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** warped and detail are bounded 3D noise samples. The vertical core stays open even at
|
||||
* maximum perturbation, while ledges and side chambers vary along the rock faces. */
|
||||
public static float density(Layout layout, int x, int y, int z, float terrain, float warped, float detail) {
|
||||
if (terrain <= 0 || !mayAffect(layout, x, z)) return terrain;
|
||||
double coarse = Math.clamp(warped, -1.0F, 1.0F), fine = Math.clamp(detail, -1.0F, 1.0F);
|
||||
double erosion = 0;
|
||||
for (Rift rift : layout.rifts()) {
|
||||
erosion = Math.max(erosion, erosion(rift.main(), x, z, coarse, fine));
|
||||
for (Segment branch : rift.branches()) erosion = Math.max(erosion, erosion(branch, x, z, coarse, fine));
|
||||
}
|
||||
double radius = Math.hypot((double) x, z);
|
||||
erosion *= smoothstep((radius - INNER_UNTOUCHED_RADIUS) / 16.0)
|
||||
* smoothstep((OUTER_UNTOUCHED_RADIUS - radius) / 20.0);
|
||||
return (float) Math.max(-1.0, terrain - erosion);
|
||||
}
|
||||
|
||||
private static boolean near(Segment segment, int x, int z) {
|
||||
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
|
||||
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 10
|
||||
&& Math.abs(-rx * segment.dz() + rz * segment.dx()) <= segment.bend() * 1.3 + 10;
|
||||
}
|
||||
|
||||
private static double erosion(Segment segment, int x, int z, double coarse, double fine) {
|
||||
if (!near(segment, x, z)) return 0;
|
||||
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
|
||||
double along = rx * segment.dx() + rz * segment.dz();
|
||||
double end = Math.clamp(along, -segment.halfLength(), segment.halfLength());
|
||||
double sideways = -rx * segment.dz() + rz * segment.dx() - curve(segment, end);
|
||||
sideways -= coarse * 1.05 + fine * 0.30;
|
||||
double width = Math.clamp(segment.halfWidth() + Math.sin(end / 13.0 + segment.phase()) * 0.65
|
||||
+ Math.sin(end / 5.7 + segment.phase() * 1.3) * 0.35 + coarse * 0.65 + fine * 0.2, 2.3, 5.7);
|
||||
double distance = Math.hypot(sideways, along - end);
|
||||
double core = Math.max(2.3, width - 1.0), outer = width + 1.6;
|
||||
return 2.1 * smoothstep((outer - distance) / (outer - core));
|
||||
}
|
||||
|
||||
private static double curve(Segment segment, double along) {
|
||||
double t = along / (segment.halfLength() * 2.0);
|
||||
return segment.bend() * (Math.sin(t * Math.PI * 2 + segment.phase()) - Math.sin(segment.phase())) * 0.5
|
||||
+ segment.bend() * 0.23 * Math.sin(t * Math.PI * 6);
|
||||
}
|
||||
|
||||
private static double smoothstep(double value) {
|
||||
double t = Math.clamp(value, 0, 1);
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
private static double unit(long seed) { return (mix(seed) >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ value >>> 27) * 0x94d049bb133111ebL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
|
||||
/** A few plants on existing suitable shores, after the forest has been decorated. */
|
||||
public final class RiftShoreSugarCaneFeature implements Feature {
|
||||
public static final MapCodec<RiftShoreSugarCaneFeature> CODEC = MapCodec.unit(RiftShoreSugarCaneFeature::new);
|
||||
|
||||
@Override
|
||||
public MapCodec<RiftShoreSugarCaneFeature> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !RiftHydrologyRuntime.enabled(noise)) return false;
|
||||
var plan = RiftHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState());
|
||||
var cane = Blocks.SUGAR_CANE.defaultBlockState();
|
||||
boolean placed = false;
|
||||
for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) {
|
||||
if (cell.hasWater() || random.nextInt(7) != 0) continue;
|
||||
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
|
||||
if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue;
|
||||
int height = 1 + random.nextInt(3);
|
||||
for (int y = 0; y < height; y++) {
|
||||
BlockPos pos = base.above(y);
|
||||
if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break;
|
||||
setBlock(level, pos, cane);
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
return placed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VegetationBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** One-time alpha.8 outlet finishing, using vanilla's persisted and consumed generation queue. */
|
||||
public final class RiftSpringOutlets {
|
||||
public record Pending(List<RiftHydrology.Position> sources, List<RiftHydrology.Position> outlets) {
|
||||
public static final Pending EMPTY = new Pending(List.of(), List.of());
|
||||
public Pending { sources = List.copyOf(sources); outlets = List.copyOf(outlets); }
|
||||
public boolean isEmpty() { return sources.isEmpty() && outlets.isEmpty(); }
|
||||
}
|
||||
|
||||
private RiftSpringOutlets() {}
|
||||
|
||||
/** Called before vanilla consumes the queue; a fully processed/reloaded chunk has no work. */
|
||||
public static Pending capturePending(ServerLevel level, LevelChunk chunk) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|
||||
|| !RiftHydrologyRuntime.enabled(noise)) return Pending.EMPTY;
|
||||
boolean queued = false;
|
||||
for (var section : chunk.getPostProcessing()) if (section != null && !section.isEmpty()) {
|
||||
queued = true;
|
||||
break;
|
||||
}
|
||||
if (!queued) return Pending.EMPTY;
|
||||
var plan = RiftHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
|
||||
List<RiftHydrology.Position> sources = new ArrayList<>(), outlets = new ArrayList<>();
|
||||
for (var spring : plan.springs()) {
|
||||
if (markedHere(chunk, spring.source())) sources.add(spring.source());
|
||||
if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet());
|
||||
}
|
||||
for (var fall : RiftHydrologyRuntime.lavaPlan(noise, level.getChunkSource().randomState()).falls()) {
|
||||
var source = new RiftHydrology.Position(fall.source().x(), fall.source().y(), fall.source().z());
|
||||
var outlet = new RiftHydrology.Position(fall.outlet().x(), fall.outlet().y(), fall.outlet().z());
|
||||
if (markedHere(chunk, source)) sources.add(source);
|
||||
if (markedHere(chunk, outlet)) outlets.add(outlet);
|
||||
}
|
||||
return new Pending(sources, outlets);
|
||||
}
|
||||
|
||||
/** All neighbouring FEATURES have finished before a chunk reaches this stage. */
|
||||
public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) {
|
||||
if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|
||||
|| !RiftHydrologyRuntime.enabled(noise)) return;
|
||||
var random = level.getChunkSource().randomState();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
for (var outlet : pending.outlets()) {
|
||||
if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet);
|
||||
var modified = RiftHydrologyRuntime.plan(noise, random).cellAt(outlet.x(), outlet.z());
|
||||
boolean carved = modified != null && outlet.y() > modified.waterY() && outlet.y() > modified.bedY()
|
||||
&& outlet.y() <= modified.carveTop();
|
||||
if (!carved && random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) {
|
||||
throw new IllegalStateException("A spring outlet must already be natural air: " + outlet);
|
||||
}
|
||||
BlockPos pos = blockPos(outlet);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (removableDecoration(current)) {
|
||||
// Only this declared air cell is reopened. Neighbour notification wakes the
|
||||
// adjacent source even if its own chunk was post-processed first.
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
for (var source : pending.sources()) {
|
||||
if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source);
|
||||
BlockPos pos = blockPos(source);
|
||||
BlockState current = chunk.getBlockState(pos);
|
||||
if (current.getFluidState().isSource()) {
|
||||
if (current.is(Blocks.WATER)) level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
|
||||
else if (current.is(Blocks.LAVA)) level.scheduleTick(pos, Fluids.LAVA, Fluids.LAVA.getTickDelay(level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean markedHere(LevelChunk chunk, RiftHydrology.Position position) {
|
||||
if (!owns(chunk, position)) return false;
|
||||
var queue = chunk.getPostProcessing()[chunk.getSectionIndex(position.y())];
|
||||
return queue != null && queue.contains(ProtoChunk.packOffsetCoordinates(blockPos(position)));
|
||||
}
|
||||
|
||||
private static boolean owns(LevelChunk chunk, RiftHydrology.Position position) {
|
||||
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
|
||||
}
|
||||
|
||||
private static BlockPos blockPos(RiftHydrology.Position position) {
|
||||
return new BlockPos(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private static boolean removableDecoration(BlockState state) {
|
||||
if (state.hasBlockEntity()) return false;
|
||||
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|
||||
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,9 @@ public final class SanctuarySpawn {
|
||||
public static final ResourceKey<NoiseGeneratorSettings> WOODLAND_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_woodland"));
|
||||
|
||||
public static final ResourceKey<NoiseGeneratorSettings> RIFT_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_rift"));
|
||||
|
||||
private SanctuarySpawn() {}
|
||||
|
||||
public static boolean usesSanctuaryGenerator(ServerLevel level) {
|
||||
@@ -39,7 +42,8 @@ public final class SanctuarySpawn {
|
||||
|| generator.generatorSettings().is(HYDROLOGY_SETTINGS)
|
||||
|| generator.generatorSettings().is(NATURAL_SETTINGS)
|
||||
|| generator.generatorSettings().is(LAYERED_SETTINGS)
|
||||
|| generator.generatorSettings().is(WOODLAND_SETTINGS));
|
||||
|| generator.generatorSettings().is(WOODLAND_SETTINGS)
|
||||
|| generator.generatorSettings().is(RIFT_SETTINGS));
|
||||
}
|
||||
|
||||
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
|
||||
|
||||
@@ -19,5 +19,13 @@
|
||||
"biome.sanctuary.woodland_rocky_heath": "Rocky Heath",
|
||||
"biome.sanctuary.woodland_dark_grove": "Dark Undergrowth",
|
||||
"biome.sanctuary.woodland_bamboo_grove": "Bamboo Grove",
|
||||
"biome.sanctuary.woodland_sulfur_depths": "Sulfur Depths"
|
||||
"biome.sanctuary.woodland_sulfur_depths": "Sulfur Depths",
|
||||
"biome.sanctuary.rift_oak_forest": "Sanctuary Oak Woodland",
|
||||
"biome.sanctuary.rift_birch_forest": "Sanctuary Birch Woodland",
|
||||
"biome.sanctuary.rift_clearing": "Flowered Clearing",
|
||||
"biome.sanctuary.rift_dry_woodland": "Dry Woodland",
|
||||
"biome.sanctuary.rift_rocky_heath": "Rocky Heath",
|
||||
"biome.sanctuary.rift_dark_grove": "Dark Undergrowth",
|
||||
"biome.sanctuary.rift_bamboo_grove": "Bamboo Grove",
|
||||
"biome.sanctuary.rift_sulfur_depths": "Sulfur Depths"
|
||||
}
|
||||
|
||||
@@ -19,5 +19,13 @@
|
||||
"biome.sanctuary.woodland_rocky_heath": "Lande rocheuse",
|
||||
"biome.sanctuary.woodland_dark_grove": "Sous-bois sombre",
|
||||
"biome.sanctuary.woodland_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.woodland_sulfur_depths": "Profondeurs soufrées"
|
||||
"biome.sanctuary.woodland_sulfur_depths": "Profondeurs soufrées",
|
||||
"biome.sanctuary.rift_oak_forest": "Chênaie de Sanctuary",
|
||||
"biome.sanctuary.rift_birch_forest": "Bétulaie de Sanctuary",
|
||||
"biome.sanctuary.rift_clearing": "Clairière fleurie",
|
||||
"biome.sanctuary.rift_dry_woodland": "Bois sec",
|
||||
"biome.sanctuary.rift_rocky_heath": "Lande rocheuse",
|
||||
"biome.sanctuary.rift_dark_grove": "Sous-bois sombre",
|
||||
"biome.sanctuary.rift_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.rift_sulfur_depths": "Profondeurs soufrées"
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:deepslate",
|
||||
"minecraft:blackstone",
|
||||
"minecraft:basalt"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"values": [
|
||||
"#sanctuary:rift_upper_ore_replaceables",
|
||||
"#sanctuary:rift_deep_ore_replaceables"
|
||||
]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:stone",
|
||||
"minecraft:granite",
|
||||
"minecraft:diorite",
|
||||
"minecraft:andesite",
|
||||
"minecraft:tuff",
|
||||
"minecraft:cobblestone",
|
||||
"minecraft:mossy_cobblestone",
|
||||
"minecraft:packed_mud"
|
||||
]
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.sparse_jungle"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.95,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.7,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:wildflowers_birch_forest",
|
||||
"minecraft:trees_birch",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.meadow"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 2,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.75,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:patch_tall_grass_2",
|
||||
"minecraft:patch_grass_meadow",
|
||||
"minecraft:flower_meadow",
|
||||
"minecraft:trees_meadow",
|
||||
"minecraft:wildflowers_meadow",
|
||||
"minecraft:flower_flower_forest",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.9,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.4,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"sanctuary:rift_sparse_trees",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.75
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.8,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:trees_birch_and_oak_leaf_litter",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.meadow"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 2,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.65,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": 4,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.5,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:rift_ore_coal",
|
||||
"sanctuary:rift_ore_iron",
|
||||
"sanctuary:rift_ore_copper",
|
||||
"sanctuary:rift_ore_gold",
|
||||
"sanctuary:rift_ore_redstone",
|
||||
"sanctuary:rift_ore_lapis",
|
||||
"sanctuary:rift_ore_diamond",
|
||||
"sanctuary:rift_ore_emerald",
|
||||
"sanctuary:rift_ore_coal_surface",
|
||||
"sanctuary:rift_ore_iron_surface",
|
||||
"sanctuary:rift_ore_copper_surface",
|
||||
"sanctuary:rift_ore_coal_ledges",
|
||||
"sanctuary:rift_ore_iron_ledges",
|
||||
"sanctuary:rift_ore_copper_ledges"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"sanctuary:rift_groves",
|
||||
"sanctuary:rift_shore_sugar_cane"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "sanctuary:rift_island",
|
||||
"terrain": "sanctuary:final_density_384",
|
||||
"warp": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_warp",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0,
|
||||
"size": 9,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:coal_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_coal_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0,
|
||||
"size": 7,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:copper_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_copper_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.25,
|
||||
"size": 4,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:diamond_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_diamond_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0,
|
||||
"size": 3,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:emerald_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_emerald_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.15,
|
||||
"size": 5,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:gold_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_gold_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0,
|
||||
"size": 7,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:iron_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_iron_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.15,
|
||||
"size": 4,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:lapis_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_lapis_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:ore",
|
||||
"discard_chance_on_air_exposure": 0.15,
|
||||
"size": 5,
|
||||
"targets": [
|
||||
{
|
||||
"state": "minecraft:redstone_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_upper_ore_replaceables"
|
||||
}
|
||||
},
|
||||
{
|
||||
"state": "minecraft:deepslate_redstone_ore",
|
||||
"target": {
|
||||
"predicate_type": "minecraft:tag_match",
|
||||
"tag": "sanctuary:rift_deep_ore_replaceables"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.3,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:blackstone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": 0.3,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:basalt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:deepslate"
|
||||
}
|
||||
]
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 136
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:rift_deep_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 152
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 168
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_deep_strata"
|
||||
}
|
||||
},
|
||||
"sanctuary:rift_upper_strata"
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.36,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:cobblestone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": 0.36,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:packed_mud"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": -0.14,
|
||||
"max_threshold": 0.14,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:mossy_cobblestone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:tuff"
|
||||
}
|
||||
]
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:rift_sulfur_depths"
|
||||
},
|
||||
"then_run": "sanctuary:rift_sulfur_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 136
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:rift_deep_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 152
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 168
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_deep_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:rift_dark_grove"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 192
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:rift_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 208
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_humid_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 224
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_humid_strata"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:rift_bamboo_grove"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 192
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": "sanctuary:rift_humid_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 208
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": 0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_humid_strata"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:not",
|
||||
"invert": {
|
||||
"type": "minecraft:y_above",
|
||||
"add_stone_depth": false,
|
||||
"anchor": {
|
||||
"absolute": 224
|
||||
},
|
||||
"surface_depth_multiplier": 0
|
||||
}
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_depth_boundary",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.45,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": "sanctuary:rift_humid_strata"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:rift_rocky_heath"
|
||||
},
|
||||
"then_run": "sanctuary:rift_upper_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_exposure",
|
||||
"min_threshold": 0.85,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": false
|
||||
},
|
||||
"then_run": "sanctuary:rift_upper_strata"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:biome",
|
||||
"biome_is": "sanctuary:rift_dry_woodland"
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_exposure",
|
||||
"min_threshold": 0.15,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": false
|
||||
},
|
||||
"then_run": "sanctuary:rift_upper_strata"
|
||||
}
|
||||
},
|
||||
"sanctuary:starter_island",
|
||||
"sanctuary:rift_upper_strata"
|
||||
]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": -0.15,
|
||||
"max_threshold": 0.55,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:sulfur"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": -0.55,
|
||||
"max_threshold": -0.3,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:cinnabar"
|
||||
}
|
||||
},
|
||||
"sanctuary:rift_depth_strata"
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"type": "minecraft:sequence",
|
||||
"sequence": [
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": -1000,
|
||||
"max_threshold": -0.36,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:granite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": 0.36,
|
||||
"max_threshold": 1000,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:diorite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:condition",
|
||||
"if_true": {
|
||||
"type": "minecraft:noise_threshold",
|
||||
"noise": "sanctuary:rift_strata",
|
||||
"min_threshold": -0.14,
|
||||
"max_threshold": 0.14,
|
||||
"is_3d": true
|
||||
},
|
||||
"then_run": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:andesite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"result_state": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -6,
|
||||
"octave_count": 2
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_octave": -3,
|
||||
"octave_count": 2,
|
||||
"amplitude_modifiers": [1.0, 0.5]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -7,
|
||||
"octave_count": 2
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_amplitude": 1.0,
|
||||
"base_octave": -6,
|
||||
"octave_count": 2
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"base_octave": -5,
|
||||
"octave_count": 3,
|
||||
"amplitude_modifiers": [1.0, 0.5, 0.25]
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"default_block": "minecraft:stone",
|
||||
"default_fluid": "minecraft:air",
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:rift_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
"chunk_surface_level": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": "sanctuary:final_density_rift",
|
||||
"ridges": 0.0,
|
||||
"temperature": "sanctuary:temperate_variation",
|
||||
"vegetation": "sanctuary:temperate_moisture"
|
||||
},
|
||||
"sea_level": -64,
|
||||
"spawn_target": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:rift_groves"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_coal",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 96
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 304
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_coal",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:uniform",
|
||||
"min_inclusive": {
|
||||
"absolute": 64
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 288
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block_predicate_filter",
|
||||
"predicate": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "minecraft:air"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:environment_scan",
|
||||
"direction_of_search": "down",
|
||||
"max_steps": 32,
|
||||
"target_condition": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "sanctuary:rift_ore_replaceables"
|
||||
},
|
||||
"allowed_search_condition": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "minecraft:air"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:offset",
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_coal",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:heightmap",
|
||||
"heightmap": "OCEAN_FLOOR_WG"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block_predicate_filter",
|
||||
"predicate": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "sanctuary:rift_ore_replaceables",
|
||||
"offset": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_copper",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 96
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 280
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_copper",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:uniform",
|
||||
"min_inclusive": {
|
||||
"absolute": 64
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 288
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block_predicate_filter",
|
||||
"predicate": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "minecraft:air"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:environment_scan",
|
||||
"direction_of_search": "down",
|
||||
"max_steps": 32,
|
||||
"target_condition": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "sanctuary:rift_ore_replaceables"
|
||||
},
|
||||
"allowed_search_condition": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "minecraft:air"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:offset",
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_copper",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:heightmap",
|
||||
"heightmap": "OCEAN_FLOOR_WG"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block_predicate_filter",
|
||||
"predicate": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "sanctuary:rift_ore_replaceables",
|
||||
"offset": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_diamond",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:rarity_filter",
|
||||
"chance": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 80
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 208
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_emerald",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 104
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 224
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_gold",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 88
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 224
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_iron",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 88
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 280
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_iron",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:uniform",
|
||||
"min_inclusive": {
|
||||
"absolute": 64
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 288
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block_predicate_filter",
|
||||
"predicate": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "minecraft:air"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:environment_scan",
|
||||
"direction_of_search": "down",
|
||||
"max_steps": 32,
|
||||
"target_condition": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "sanctuary:rift_ore_replaceables"
|
||||
},
|
||||
"allowed_search_condition": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "minecraft:air"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:offset",
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_iron",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:heightmap",
|
||||
"heightmap": "OCEAN_FLOOR_WG"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:block_predicate_filter",
|
||||
"predicate": {
|
||||
"type": "minecraft:matching_block_tag",
|
||||
"tag": "sanctuary:rift_ore_replaceables",
|
||||
"offset": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_lapis",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 104
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 240
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature": "sanctuary:rift_ore_redstone",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"min_inclusive": {
|
||||
"absolute": 88
|
||||
},
|
||||
"max_inclusive": {
|
||||
"absolute": 216
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"feature": {
|
||||
"type": "sanctuary:rift_shore_sugar_cane"
|
||||
},
|
||||
"placement": []
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"feature": "minecraft:trees_birch_and_oak_leaf_litter",
|
||||
"placement": [
|
||||
{
|
||||
"type": "minecraft:count",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:in_square"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:surface_water_depth_filter",
|
||||
"max_water_depth": 0
|
||||
},
|
||||
{
|
||||
"type": "minecraft:heightmap",
|
||||
"heightmap": "OCEAN_FLOOR"
|
||||
},
|
||||
{
|
||||
"type": "minecraft:biome"
|
||||
}
|
||||
]
|
||||
}
|
||||
+10
-10
@@ -5,17 +5,17 @@
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "sanctuary:woodland_island",
|
||||
"oak_forest": "sanctuary:woodland_oak_forest",
|
||||
"birch_forest": "sanctuary:woodland_birch_forest",
|
||||
"clearing": "sanctuary:woodland_clearing",
|
||||
"dry_woodland": "sanctuary:woodland_dry_woodland",
|
||||
"dark_grove": "sanctuary:woodland_dark_grove",
|
||||
"bamboo_grove": "sanctuary:woodland_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:woodland_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:woodland_rocky_heath"
|
||||
"type": "sanctuary:rift_island",
|
||||
"oak_forest": "sanctuary:rift_oak_forest",
|
||||
"birch_forest": "sanctuary:rift_birch_forest",
|
||||
"clearing": "sanctuary:rift_clearing",
|
||||
"dry_woodland": "sanctuary:rift_dry_woodland",
|
||||
"dark_grove": "sanctuary:rift_dark_grove",
|
||||
"bamboo_grove": "sanctuary:rift_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:rift_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:rift_rocky_heath"
|
||||
},
|
||||
"settings": "sanctuary:sanctuary_woodland"
|
||||
"settings": "sanctuary:sanctuary_rift"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"InitialSpawnMixin",
|
||||
"SurfaceHydrologyMixin",
|
||||
"LayeredSpringOutletMixin",
|
||||
"WoodlandSpringOutletMixin"
|
||||
"WoodlandSpringOutletMixin",
|
||||
"RiftSpringOutletMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.SanctuarySkyMixin",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Behavioral fixture: a thick uneven plateau can host a real connected river; void cannot. */
|
||||
public final class RiftHydrologySmoke {
|
||||
public static void main(String[] args) {
|
||||
RiftHydrology.Sampler plateau = (x, y, z) -> {
|
||||
int top = 248 + (int) (Math.sin(x / 33.0) * 4 + Math.cos(z / 41.0) * 4);
|
||||
return x * x + z * z < 205 * 205 && y >= 100 && y <= top ? 1 : -1;
|
||||
};
|
||||
var plan = RiftHydrology.create(42, plateau);
|
||||
var river = plan.features().stream().filter(f -> f.kind() == RiftHydrology.Kind.RIVER).findFirst()
|
||||
.orElseThrow(() -> new AssertionError("A supported plateau must permit a continuous river"));
|
||||
if (river.path().size() < 140) throw new AssertionError("River is too short");
|
||||
for (var point : river.path()) {
|
||||
var cell = plan.cellAt(point.x(), point.z());
|
||||
if (cell == null || !cell.hasWater() || cell.featureId() != river.id()) throw new AssertionError("Dry gap in river");
|
||||
}
|
||||
for (var cell : plan.cells()) {
|
||||
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++)
|
||||
if (plateau.sample(cell.x(), y, cell.z()) <= 0) throw new AssertionError("Artificial foundation");
|
||||
if (cell.carveTop() - cell.bedY() > RiftHydrology.MAX_CARVE) throw new AssertionError("Excavation exceeds budget");
|
||||
}
|
||||
var replay = RiftHydrology.create(42, plateau);
|
||||
if (!plan.features().equals(replay.features()) || !java.util.List.copyOf(plan.cells()).equals(java.util.List.copyOf(replay.cells())))
|
||||
throw new AssertionError("Same seed and terrain must replay exactly");
|
||||
var empty = RiftHydrology.create(42, (x,y,z) -> -1);
|
||||
if (!empty.cells().isEmpty() || !empty.springs().isEmpty()) throw new AssertionError("Void must remain void");
|
||||
System.out.println("Rift hydrology: connected river, natural supports, bounded carving, deterministic replay and empty void passed; river=" + river.path().size() + ", wet=" + river.waterCells());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
/** Pure geometry regressions; actual fluid propagation is covered separately in Minecraft. */
|
||||
public final class RiftLavaSmoke {
|
||||
private RiftLavaSmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
RiftHydrology.Plan water = RiftHydrology.create(0, (x, y, z) -> -1);
|
||||
RiftLavaDeposit.Plan empty = RiftLavaDeposit.create(0, (x, y, z) -> -1, water);
|
||||
check(empty.cells().isEmpty() && empty.falls().isEmpty(), "void cannot create lava sites");
|
||||
check(RiftLavaFlows.create(0, (x, y, z) -> 1, water, empty).isEmpty(),
|
||||
"sealed rock cannot create an outlet");
|
||||
|
||||
RiftHydrology.Sampler wall = (x, y, z) ->
|
||||
x <= -32 && x >= -176 && Math.abs(z) <= 152 && y >= 48 && y <= 200 ? 1 : -1;
|
||||
var plan = RiftLavaDeposit.create(42, wall, water);
|
||||
check(plan.falls().size() == 2, "two natural wall outlets expected on reference wall");
|
||||
check(plan.falls().equals(RiftLavaDeposit.create(42, wall, water).falls()), "same seed must reproduce all sites");
|
||||
check(!plan.falls().equals(RiftLavaDeposit.create(43, wall, water).falls()), "seed must affect selection");
|
||||
check(plan.lavaCells().size() == 2 && plan.cells().size() == 2,
|
||||
"each seep writes only its rock source, never a filled falling column");
|
||||
for (var cell : plan.cells()) check(wall.sample(cell.x(), cell.y(), cell.z()) > 0,
|
||||
"every changed cell must start as natural rock");
|
||||
var supports = new HashSet<>(plan.supports());
|
||||
for (var support : supports) check(wall.sample(support.x(), support.y(), support.z()) > 0,
|
||||
"source protection must be existing rock");
|
||||
for (var fall : plan.falls()) {
|
||||
validate(fall, wall);
|
||||
check(fall.flowBounds().minY() == 0, "reference wall must allow a cascade into the void");
|
||||
check(!supports.contains(fall.source()), "source itself must not be an unchanged support");
|
||||
check(plan.fallsInChunk(fall.source().x() >> 4, fall.source().z() >> 4).contains(fall),
|
||||
"each source belongs to its own chunk");
|
||||
check(plan.cellsInChunk(fall.source().x() >> 4, fall.source().z() >> 4).stream()
|
||||
.anyMatch(c -> c.x() == fall.source().x() && c.y() == fall.source().y() && c.z() == fall.source().z()),
|
||||
"source chunk must receive its lava cell");
|
||||
}
|
||||
check(plan.flowSources().size() == 2 && plan.flowBounds().size() == 2, "flow metadata must cover every source");
|
||||
|
||||
// The shelf extends well beyond the three-block lava reach. A water-like reach of seven
|
||||
// would incorrectly predict a spill over its edge, rather than the small retained puddle.
|
||||
RiftHydrology.Sampler shelf = (x, y, z) -> {
|
||||
boolean upperWall = x <= -32 && x >= -176 && Math.abs(z) <= 152 && y >= 120 && y <= 200;
|
||||
boolean lowerShelf = x >= -216 && x <= -24 && Math.abs(z) <= 216 && y == 100;
|
||||
return upperWall || lowerShelf ? 1 : -1;
|
||||
};
|
||||
var shelfFalls = RiftLavaFlows.create(42, shelf, water, empty);
|
||||
check(!shelfFalls.isEmpty(), "reference shelf must receive lava");
|
||||
for (var fall : shelfFalls) {
|
||||
validate(fall, shelf);
|
||||
check(fall.flowBounds().minY() == 101, "lava must stop on a wide natural shelf within reach three");
|
||||
}
|
||||
check(RiftLavaFlows.HORIZONTAL_REACH == 3, "non-ultrawarm lava reach");
|
||||
lowerPocketPriority(water);
|
||||
System.out.println("RiftLavaSmoke: seeded deep rock sources, existing supports, chunk ownership and three-block lava reach passed.");
|
||||
}
|
||||
|
||||
private static void lowerPocketPriority(RiftHydrology.Plan water) {
|
||||
RiftHydrology.Sampler layeredCliff = (x, y, z) -> {
|
||||
if (Math.hypot(x, z) >= 220 || y < 64) return -1;
|
||||
if (x <= 0) return y <= 260 ? 1 : -1;
|
||||
return y <= 120 || y >= 200 && y <= 220 ? 1 : -1;
|
||||
};
|
||||
var plan = RiftLavaDeposit.create(0, layeredCliff, water);
|
||||
var entry = plan.access().orElseThrow(() -> new AssertionError("reference cave should contain a pocket"));
|
||||
check(entry.y() == 121, "lower cave pocket must take priority over the available upper ledge");
|
||||
check(layeredCliff.sample(entry.x(), 220, entry.z()) > 0, "lower access must retain its natural ceiling");
|
||||
check(plan.lavaCells().size() - plan.falls().size() == 18, "lower pocket keeps exactly eighteen sources");
|
||||
for (int y = entry.y(); y < entry.y() + 3; y++) check(layeredCliff.sample(entry.x(), y, entry.z()) <= 0,
|
||||
"lower pocket entrance must have three blocks of natural headroom");
|
||||
for (int y = entry.y() - 3; y < entry.y(); y++) check(layeredCliff.sample(entry.x(), y, entry.z()) > 0,
|
||||
"lower pocket entrance must stand on three natural blocks");
|
||||
var changed = new HashSet<RiftLavaDeposit.Position>();
|
||||
for (var cell : plan.cells()) {
|
||||
check(layeredCliff.sample(cell.x(), cell.y(), cell.z()) > 0, "lower chamber only removes existing rock");
|
||||
changed.add(new RiftLavaDeposit.Position(cell.x(), cell.y(), cell.z()));
|
||||
}
|
||||
for (var support : plan.supports()) {
|
||||
check(layeredCliff.sample(support.x(), support.y(), support.z()) > 0 && !changed.contains(support),
|
||||
"lower pocket shell must remain natural and unchanged");
|
||||
}
|
||||
RiftHydrology.Sampler upperOnly = (x, y, z) -> Math.hypot(x, z) < 220
|
||||
&& y >= 100 && y <= (x <= 0 ? 240 : 200) ? 1 : -1;
|
||||
var fallback = RiftLavaDeposit.create(0, upperOnly, water);
|
||||
check(fallback.access().orElseThrow().y() == 201, "existing upper ledge remains a fallback when no lower site fits");
|
||||
check(fallback.lavaCells().size() - fallback.falls().size() == 18, "fallback keeps its original pocket volume");
|
||||
}
|
||||
|
||||
private static void validate(RiftLavaFlows.Fall fall, RiftHydrology.Sampler sampler) {
|
||||
check(fall.source().y() >= 40 && fall.source().y() <= 160, "source altitude");
|
||||
check(sampler.sample(fall.source().x(), fall.source().y(), fall.source().z()) > 0, "rock source");
|
||||
check(sampler.sample(fall.outlet().x(), fall.outlet().y(), fall.outlet().z()) <= 0, "naturally open outlet");
|
||||
var path = fall.flowPath();
|
||||
check(path.getFirst().equals(fall.source()) && path.get(1).equals(fall.outlet()), "explicit source/outlet path");
|
||||
check(fall.source().y() - path.getLast().y() >= 12, "genuine falling path");
|
||||
for (int i = 1; i < path.size(); i++) {
|
||||
var a = path.get(i - 1); var b = path.get(i);
|
||||
check(Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()) + Math.abs(a.z() - b.z()) == 1,
|
||||
"flow path must use adjacent voxels");
|
||||
check(b.y() <= a.y(), "lava cannot ascend");
|
||||
check(sampler.sample(b.x(), b.y(), b.z()) <= 0, "flow must stay outside natural rock");
|
||||
var bounds = fall.flowBounds();
|
||||
check(b.x() >= bounds.minX() && b.x() <= bounds.maxX() && b.y() >= bounds.minY()
|
||||
&& b.y() <= bounds.maxY() && b.z() >= bounds.minZ() && b.z() <= bounds.maxZ(), "path within declared bounds");
|
||||
}
|
||||
}
|
||||
|
||||
private static void check(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
/** Alpha.8 regression: richer reachable depths retain the complete alpha.7 upper climate. */
|
||||
public final class RiftPaletteSmoke {
|
||||
public static void main(String[] args) {
|
||||
for (int y = 216; y < 384; y += 4) {
|
||||
for (int variation = -20; variation <= 20; variation++) {
|
||||
for (int moisture = -20; moisture <= 20; moisture++) {
|
||||
float v = variation / 20.0F, m = moisture / 20.0F;
|
||||
require(RiftBiomePalette.select(y, v, m).name()
|
||||
.equals(WoodlandBiomePalette.select(y, v, m).name()),
|
||||
"The established upper woodland palette must remain unchanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
require(RiftBiomePalette.select(176, 0.25F, 0.1F) == RiftBiomePalette.Kind.SULFUR_DEPTHS,
|
||||
"Sulfur must reach the lower ledges, rather than only the very bottom of the dimension");
|
||||
require(WoodlandBiomePalette.select(176, 0.25F, 0.1F) != WoodlandBiomePalette.Kind.SULFUR_DEPTHS,
|
||||
"The former saved climate must keep its original lower palette");
|
||||
require(RiftBiomePalette.select(144, -0.2F, 0.2F) == RiftBiomePalette.Kind.DARK_GROVE,
|
||||
"Humid woodland must remain available beside the mineral province");
|
||||
require(RiftBiomePalette.select(144, -0.6F, 0.7F) == RiftBiomePalette.Kind.BAMBOO_GROVE,
|
||||
"Rare bamboo pockets must remain possible in the humid depths");
|
||||
EnumSet<RiftBiomePalette.Kind> kinds = EnumSet.noneOf(RiftBiomePalette.Kind.class);
|
||||
int oldSulfur = 0, newSulfur = 0;
|
||||
for (int y = 64; y < 216; y += 8) {
|
||||
for (int variation = -10; variation <= 10; variation++) {
|
||||
for (int moisture = -10; moisture <= 10; moisture++) {
|
||||
float v = variation / 10.0F, m = moisture / 10.0F;
|
||||
RiftBiomePalette.Kind selected = RiftBiomePalette.select(y, v, m);
|
||||
kinds.add(selected);
|
||||
if (selected == RiftBiomePalette.Kind.SULFUR_DEPTHS) newSulfur++;
|
||||
if (WoodlandBiomePalette.select(y, v, m) == WoodlandBiomePalette.Kind.SULFUR_DEPTHS) oldSulfur++;
|
||||
}
|
||||
}
|
||||
}
|
||||
require(kinds.equals(EnumSet.allOf(RiftBiomePalette.Kind.class)),
|
||||
"All eight palettes must remain reachable within the island's range");
|
||||
require(newSulfur > oldSulfur * 4,
|
||||
"The sulfur climate must be substantially more accessible without a per-world quota");
|
||||
System.out.println("RiftPaletteSmoke: preserved upper forests, reachable sulfur and humid lower palettes passed.");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Subtraction, vertical passage and bounded geometry without Minecraft or a world save. */
|
||||
public final class RiftShapeSmoke {
|
||||
public static void main(String[] args) {
|
||||
for (long seed : new long[]{0, 42, 8675309, Long.MIN_VALUE, Long.MAX_VALUE}) {
|
||||
var layout = RiftShape.layout(seed);
|
||||
require(layout.equals(RiftShape.layout(seed)), "The same seed must reproduce identical fractures");
|
||||
require(layout.rifts().size() >= 1 && layout.rifts().size() <= 3, "There should be only a few major fractures");
|
||||
int originalSolid = 0, removed = 0;
|
||||
for (int x = -288; x <= 288; x += 2) for (int z = -288; z <= 288; z += 2) {
|
||||
float input = Math.hypot(x, z) < 220 ? 0.65F : -1.0F;
|
||||
float output = RiftShape.density(layout, x, 200, z, input,
|
||||
(float) Math.sin(x / 31.0 + z / 19.0), (float) Math.cos(x / 7.0 - z / 11.0));
|
||||
require(output <= input, "Fractures must never add terrain");
|
||||
require(Float.isFinite(output), "Density must stay finite");
|
||||
if (input < 0) require(output == input, "Existing void must be preserved exactly");
|
||||
if (Math.hypot(x, z) >= RiftShape.OUTER_UNTOUCHED_RADIUS || Math.hypot(x, z) <= RiftShape.INNER_UNTOUCHED_RADIUS)
|
||||
require(output == input, "Keep the far coast and central neighbourhood unchanged");
|
||||
if (input > 0) { originalSolid++; if (output <= 0) removed++; }
|
||||
}
|
||||
require(removed > 100, "Fractures must actually cut a solid volume");
|
||||
require(removed < originalSolid * 0.09, "Most of the existing island mass must be preserved");
|
||||
for (var rift : layout.rifts()) {
|
||||
var center = RiftShape.centerline(rift.main(), 0);
|
||||
int x = (int) Math.round(center.x()), z = (int) Math.round(center.z());
|
||||
require(rift.main().halfLength() * 2 >= 80 && rift.main().halfLength() * 2 <= 160, "Major fracture length is bounded");
|
||||
for (int y = 1; y < 384; y++) for (float warp : new float[]{-1, 1}) for (float detail : new float[]{-1, 1}) {
|
||||
require(RiftShape.density(layout, x, y, z, 1, warp, detail) < 0,
|
||||
"A real continuous vertical core must cross even maximally dense rock");
|
||||
}
|
||||
for (var branch : rift.branches()) {
|
||||
var start = RiftShape.centerline(branch, -branch.halfLength());
|
||||
require(RiftShape.density(layout, (int) Math.round(start.x()), 170, (int) Math.round(start.z()), 1, 0, 0) < 0,
|
||||
"A branch must join its parent fracture without a detached drilled shaft");
|
||||
}
|
||||
}
|
||||
}
|
||||
require(!RiftShape.layout(0).equals(RiftShape.layout(42)), "The world seed must change fracture locations");
|
||||
var layout = RiftShape.layout(0);
|
||||
for (int x : new int[]{Integer.MIN_VALUE, Integer.MAX_VALUE}) for (int z : new int[]{Integer.MIN_VALUE, Integer.MAX_VALUE})
|
||||
require(RiftShape.density(layout, x, 128, z, -1, 1, -1) == -1, "Extreme coordinates must preserve void without overflow");
|
||||
System.out.println("RiftShapeSmoke: subtractive branching fractures, through-passages and preserved island mass passed.");
|
||||
}
|
||||
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.7"
|
||||
version = "0.1.0-alpha.8"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user