From 985e8b9192820981e388093294b76966b89b2fa4 Mon Sep 17 00:00:00 2001 From: koka Date: Tue, 1 Sep 2026 17:55:40 +0200 Subject: [PATCH] feat(backroom): stack five dark generation levels --- CHANGELOG.md | 11 + README.md | 2 + build.gradle | 49 +- gradle.properties | 2 +- sanctuary/BACKROOM.md | 45 +- sanctuary/build.gradle | 42 +- .../sanctuary/backroom/BackroomService.java | 42 +- .../feature/BackroomArchitectureFeature.java | 519 +++++++++++++----- .../worldgen/feature/BackroomLayout.java | 155 ++++++ .../assets/sanctuary/lang/en_us.json | 2 +- .../assets/sanctuary/lang/fr_fr.json | 2 +- .../assets/sanctuary/lang/ru_ru.json | 2 +- .../sanctuary/dimension_type/backroom.json | 24 +- .../sanctuary/worldgen/biome/backroom.json | 12 +- .../worldgen/noise_settings/backroom.json | 6 +- .../backroom/BackroomModelSmoke.java | 81 ++- 16 files changed, 757 insertions(+), 239 deletions(-) create mode 100644 sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 86279b0..c3cf2fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve - Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46. - Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées. +## Non publié + +Module modifié : **Sanctuary `0.0.0-alpha.132`**. + +- étend `sanctuary:backroom` à toute la hauteur Y -64 → 383 et empile cinq niveaux thématiques de haut en bas : seuil modulaire, archives, ville, services et glitch ; +- remplace les volumes ouverts par des pièces et couloirs raccordés, quatre angles de salle déclinés en cinq matériaux, des bibliothèques immenses, des tours plus serrées, des autoroutes continues et de rares parcs ; +- ajoute au niveau inférieur une grille de prismes 3D et un labyrinthe déterministe connecté, avec portes physiquement ouvertes, salles fusionnées et trous volontaires vers le vide ; +- fait correspondre exactement chaque ouverture entre le sol supérieur et le plafond inférieur, place une torche récupérable à chaque arrivée et raréfie fortement les lits de sortie ; +- supprime le ciel, les astres, la lumière ambiante et tous les monstres, y compris ceux injectés par un autre système ; +- conserve les IDs, les données Sanctuary v14 et le protocole réseau 24 ; seuls les nouveaux chunks Backroom utilisent la nouvelle architecture complète. + ## `26.2.0-alpha.227` Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-alpha.57`**. diff --git a/README.md b/README.md index 8efdfb9..d059183 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans Version courante du pack distribué : **`26.2.0-alpha.227`**. Cette release distribue Another World **`0.0.0-alpha.57`** et Sanctuary **`0.0.0-alpha.131`** ; les huit autres modules conservent leur version issue de l'alpha.226. +Le prochain module Sanctuary **`0.0.0-alpha.132`** reconstruit la Backroom sur cinq niveaux empilés entre Y -64 et Y 383. Le monde devient entièrement noir et sans monstres ; l’arrivée est signalée par une torche récupérable. Les nouveaux chunks composent seuils modulaires, bibliothèques et bureaux, ville dense avec autoroutes et parcs, couloirs techniques, puis une grille 3D et un labyrinthe connecté dont certains trous mènent au vide. Les chunks déjà générés restent intacts. + L’alpha.227 ajoute la dimension persistante `sanctuary:backroom`. On y entre accroupi devant un lit et on n’en ressort que par un lit. L’équipement d’entrée est transféré atomiquement vers la Mailbox, tandis que les objets perdus ou abandonnés dans la dimension alimentent des coffres renouvelables sans duplication. Les anciennes sauvegardes sont migrées vers Sanctuary data v14 sans réinitialisation ; le protocole réseau reste en version 24. L’alpha.225 corrige quatre tickets : posséder un coffre-fort devient obligatoire pour en braquer un autre, les familiers temporaires ne donnent plus de butin, d’expérience, de lootbox ni de progression de quête, les seize variantes Canaplia survivent à tout leur cycle de culture, et le Magic Carpet retrouve la texture historique du Totem de Notch à la place de la laine violette. Les IDs, sauvegardes, formats de données et protocoles restent compatibles. diff --git a/build.gradle b/build.gradle index d4a9b99..4cd5b1c 100644 --- a/build.gradle +++ b/build.gradle @@ -2916,7 +2916,7 @@ tasks.register("verifyNotchEncounter") { def textureHash = MessageDigest.getInstance("SHA-256").digest(texture.bytes).encodeHex().toString() if (rootProject.itsalive_version != "0.0.0-alpha.38" || rootProject.ouch_version != "0.0.0-alpha.11" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || rootProject.masterkey_version != "0.0.0-alpha.9" || rootProject.pack_version != "26.2.0-alpha.227" || manifests.itsalive.custom?.sanctuary26?.data_version != 4 @@ -3010,7 +3010,7 @@ tasks.register("verifyMagicCarpet") { if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" || rootProject.onlyfun_version != "0.0.0-alpha.27" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4 || manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5 || manifests.iliketomoveit.depends?.sanctuary != ">=0.0.0-alpha.127" @@ -3641,7 +3641,7 @@ tasks.register("verifyProgressivePauseMenuRelease") { "sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))] } if (rootProject.pack_version != "26.2.0-alpha.227" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || rootProject.onlyfun_version != "0.0.0-alpha.27" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"] @@ -4484,7 +4484,7 @@ tasks.register("verifyShopPricingLayoutRelease") { def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json")) if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.onlyfun_version != "0.0.0-alpha.27" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.192" || migration.target?.pack_version != "26.2.0-alpha.193" @@ -4578,7 +4578,7 @@ tasks.register("verifyShopCsvBossGatesRelease") { def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.onlyfun_version != "0.0.0-alpha.27" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.194" || migration.source?.modules != [onlyfun: "0.0.0-alpha.18", sanctuary: "0.0.0-alpha.110"] @@ -4671,7 +4671,7 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") { def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json")) if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.itsalive_version != "0.0.0-alpha.38" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.196" || migration.target?.pack_version != "26.2.0-alpha.197" || migration.target?.modules != [itsalive: "0.0.0-alpha.29", sanctuary: "0.0.0-alpha.113"] @@ -4771,7 +4771,7 @@ tasks.register("verifyQuestRequestContractsRelease") { def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json")) if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.itsalive_version != "0.0.0-alpha.38" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.198" || migration.source?.modules?.sanctuary != "0.0.0-alpha.114" || migration.target?.pack_version != "26.2.0-alpha.199" @@ -4891,7 +4891,7 @@ tasks.register("verifyQuestRequestEscrowRelease") { def packwizIndex = file("pack/packwiz/index.toml").text def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json")) if (rootProject.pack_version != "26.2.0-alpha.227" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.200" || migration.source?.modules?.sanctuary != "0.0.0-alpha.115" || migration.target?.pack_version != "26.2.0-alpha.201" @@ -4948,7 +4948,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { || rootProject.anotherworld_version != "0.0.0-alpha.57" || rootProject.itsalive_version != "0.0.0-alpha.38" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.201" || migration.source?.modules != [ itsalive: "0.0.0-alpha.30", @@ -5106,7 +5106,7 @@ tasks.register("verifySafeVaultFeature") { }.keySet() if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.anotherworld_version != "0.0.0-alpha.57" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.schema_version != 1 || migration.source?.pack_version != "26.2.0-alpha.201" || migration.target?.pack_version != "26.2.0-alpha.202" @@ -5966,7 +5966,7 @@ tasks.register("verifyAlpha210Release") { it.path == "mods/jei-26.2-fabric-30.29.0.199.jar" } if (rootProject.pack_version != "26.2.0-alpha.227" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || rootProject.onlyfun_version != "0.0.0-alpha.27" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" || rootProject.itsalive_version != "0.0.0-alpha.38" @@ -6068,7 +6068,7 @@ tasks.register("verifyAlpha211Release") { itsalive: "0.0.0-alpha.38", ouch: "0.0.0-alpha.11", masterkey: "0.0.0-alpha.9"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.132"] def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11] def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 14] def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22] @@ -6168,7 +6168,7 @@ tasks.register("verifyAlpha212Release") { it.path == "mods/jei-26.2-fabric-30.29.0.199.jar" } if (rootProject.pack_version != "26.2.0-alpha.227" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.211" || migration.source?.modules != [sanctuary: "0.0.0-alpha.119"] || migration.target?.pack_version != "26.2.0-alpha.212" @@ -6667,7 +6667,7 @@ tasks.register("verifyAlpha218Release") { } || rootProject.onlyfun_version != "0.0.0-alpha.27" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.217" || migration.source?.modules != sourceModules || migration.target?.pack_version != "26.2.0-alpha.218" @@ -6753,7 +6753,7 @@ tasks.register("verifyAlpha219Release") { ambiance: "0.0.0-alpha.22", sanctuary: "0.0.0-alpha.131", iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27", itsalive: "0.0.0-alpha.38", ouch: "0.0.0-alpha.11", masterkey: "0.0.0-alpha.9"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.132"] def requiredJars = currentModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } @@ -7078,7 +7078,7 @@ tasks.register("verifyAlpha222Release") { } if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.redstoner_version != "0.0.0-alpha.13" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.221" || migration.source?.modules != [ redstoner: "0.0.0-alpha.12", sanctuary: "0.0.0-alpha.122"] @@ -7236,7 +7236,7 @@ tasks.register("verifyAlpha223Release") { it.path == "mods/jei-26.2-fabric-30.29.0.199.jar" } if (rootProject.pack_version != "26.2.0-alpha.227" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.222" || migration.source?.modules != [sanctuary: "0.0.0-alpha.124"] || migration.target?.pack_version != "26.2.0-alpha.223" @@ -7357,7 +7357,7 @@ tasks.register("verifyAlpha224Release") { def currentModules = targetModules + [ itsalive: "0.0.0-alpha.38", iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27", sanctuary: "0.0.0-alpha.131"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.132"] def inventorySort = file( "sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryInventorySort.java").text def clientProgress = file( @@ -7479,11 +7479,11 @@ tasks.register("verifyAlpha225Release") { sanctuary: "0.0.0-alpha.129"] def currentModules = targetModules + [sanctuary: "0.0.0-alpha.131"] def currentWorkspaceModules = currentModules + [ - anotherworld: "0.0.0-alpha.57", sanctuary: "0.0.0-alpha.131"] + anotherworld: "0.0.0-alpha.57", sanctuary: "0.0.0-alpha.132"] def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [sanctuary: "0.0.0-alpha.131"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } @@ -7643,12 +7643,12 @@ tasks.register("verifyAlpha226Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [sanctuary: "0.0.0-alpha.131"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } if (rootProject.pack_version != "26.2.0-alpha.227" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.225" || migration.source?.modules != [sanctuary: "0.0.0-alpha.129"] @@ -7734,12 +7734,13 @@ tasks.register("verifyAlpha227Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def requiredJars = workspaceModules.collect { id, version -> + def releasedModules = workspaceModules + [sanctuary: "0.0.0-alpha.131"] + def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } if (rootProject.pack_version != "26.2.0-alpha.227" || rootProject.anotherworld_version != "0.0.0-alpha.57" - || rootProject.sanctuary_version != "0.0.0-alpha.131" + || rootProject.sanctuary_version != "0.0.0-alpha.132" || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.226" || migration.source?.modules != [ diff --git a/gradle.properties b/gradle.properties index 656919a..3d660a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.18 iliketomoveit_lifecycle=active onlyfun_version=0.0.0-alpha.27 onlyfun_lifecycle=active -sanctuary_version=0.0.0-alpha.131 +sanctuary_version=0.0.0-alpha.132 sanctuary_lifecycle=active pack_version=26.2.0-alpha.227 maven_group=fr.koka99cab.sanctuary26 diff --git a/sanctuary/BACKROOM.md b/sanctuary/BACKROOM.md index f7ef570..1ae608c 100644 --- a/sanctuary/BACKROOM.md +++ b/sanctuary/BACKROOM.md @@ -22,6 +22,9 @@ Sa zone architecturale forme un disque de 2 000 blocs autour de l’origine. - Toute autre téléportation vers la Backroom est refusée. Une téléportation forcée vers l’extérieur reste possible pour l’administration, mais replace d’abord le butin porté dans le registre : elle ne constitue jamais une extraction réussie. +- Chaque arrivée se fait sur le dernier plancher du niveau 0. Le serveur libère exactement les deux + blocs nécessaires au joueur et pose une torche adjacente : elle peut être récupérée et devient le + premier outil d’exploration dans une dimension autrement noire. ## Objets perdus et coffres @@ -44,19 +47,41 @@ Pendant une expédition, chaque coffre distinct est un essai personnel. Le premi de proposer une perte récente du joueur, le deuxième 90 %, le troisième 100 %, puis les suivants 65 %. Sans perte personnelle disponible, le coffre suit simplement son tirage général. -## Architecture +## Architecture verticale -Les plans sont déterministes par grandes zones et utilisent cinq familles composées : +La liste [Normal Levels I](https://backrooms-wiki.wikidot.com/normal-levels-i) sert uniquement de +référence d’ambiance et de vocabulaire. Les plans, palettes et règles ci-dessous sont originaux au +pack ; aucun texte ni visuel du wiki n’est embarqué. -- mégalopoles en béton, verre, pierre et voirie ; -- bureaux superposés en bois, clay, textile et éléments techniques ; -- plaines naturelles avec arbres et rares ruines ; -- agrégats de plateformes et de pièces flottantes reliées verticalement ; -- pièces construites visuellement à 90° ou à l’envers, sans modifier la gravité globale. +La dimension couvre maintenant toute la hauteur de **Y = -64 à Y = 383**. Ses cinq niveaux sont +numérotés de haut en bas afin qu’une chute fasse toujours avancer vers le niveau suivant : -Les matériaux très marqués d’Another World n’apparaissent que ponctuellement aux anomalies. Aucun -bloc précieux ne sert de matériau structurel ; de très rares blocs de diamant sont cachés dans les -fondations. +- **niveau 0 — Seuil, Y 295 à 383** : cinq plans de pièces jaunes et sableuses, avec couloirs, + cloisons à 90°, colonnes et quatre angles de salle. Chaque angle possède cinq variantes de + matériaux ; des trous rares relient les plans superposés ; +- **niveau 1 — Archives, Y 206 à 294** : quatre étages de bureaux et de bibliothèques pouvant + couvrir plusieurs chunks, avec longues rangées de rayonnages, grandes fenêtres intérieures, + salles de lecture et cloisons ouvertes ; +- **niveau 2 — Ville, Y 117 à 205** : bâtiments rapprochés séparés par de petites ruelles, + autoroutes orthogonales continues, tours à plusieurs étages et rares parcs enfermés sous le + plafond de la dimension ; +- **niveau 3 — Services, Y 28 à 116** : cinq plans de couloirs techniques en briques, grilles et + béton, dont toutes les jonctions de portes restent physiquement ouvertes ; +- **niveau 4 — Glitch, Y -64 à 27** : prismes de briques en grille 3D autour d’un toit où l’on peut + tomber, puis réseau déterministe de salles et couloirs. Chaque cellule du labyrinthe conserve une + route ouverte vers son origine ; certaines cloisons disparaissent pour fusionner des salles de + tailles différentes. De rares trous dans le dernier plancher mènent au vrai vide. + +Les ouvertures entre deux niveaux sont calculées une seule fois puis appliquées à la fois au sol +supérieur et au plafond inférieur : une chute annoncée ne débouche donc pas sur un bloc fermé. Les +imperfections, morceaux suspendus et raccords volontairement cassés font partie de l’esthétique, +mais les portes des couloirs restent traversables. Les lits de sortie générés sont désormais rares. + +La Backroom ne possède ni ciel, ni soleil, ni lune, ni étoiles, ni lumière ambiante. Son biome ne +déclare aucune créature et le serveur retire aussi toute entité de catégorie monstre qui y serait +injectée par un autre système. Les matériaux très marqués d’Another World n’apparaissent que +ponctuellement aux anomalies. Aucun bloc précieux ne sert de matériau structurel ; de très rares +blocs de diamant restent cachés dans les fondations. ## Exploitation diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index 218f021..04c2a04 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -199,7 +199,7 @@ tasks.register("verifyBlackMarket") { def questData = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java").text def quests = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java").text def inventoryView = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || !data.contains('SAVE_KEY = "black_market"') || !data.contains("DATA_VERSION = 2") || !data.contains("ItemStack.CODEC.fieldOf(\"item\")") || !data.contains("replaceListing") || !service.contains("LISTING_FEE_RUBIES = 1") || !service.contains("COMMISSION_PERCENT = 5") @@ -2915,7 +2915,7 @@ tasks.register("verifyShopCurrencyRowsRelease") { "src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text def shop = file( "src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.modules?.sanctuary != "0.0.0-alpha.117" || migration.shop_currency_inventory?.all_unlocked_rows_counted != true || migration.shop_currency_inventory?.overflow_rows_counted != true @@ -3081,7 +3081,7 @@ tasks.register("verifyProgressivePauseMenu") { "pack/migrations/26.2.0-alpha.190-progressive-pause-menu-to-alpha.191.json")) def pauseMenu = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/PauseScreenMixin.java").text def costs = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/progression/SanctuaryProgressionCosts.java").text - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.pack_version != "26.2.0-alpha.191" || migration.target?.modules?.sanctuary != "0.0.0-alpha.109" || migration.pause_menu?.progression_full_width != true @@ -3119,7 +3119,7 @@ tasks.register("verifyShopPricingLayout") { "pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json")) def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryShopScreen.java").text def progression = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopProgression.java").text - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.modules?.sanctuary != "0.0.0-alpha.110" || migration.shop_layout?.locked_offer_buttons != 6 || migration.shop_layout?.button_slots != [4, 5, 6, 7, 8, 9] @@ -3150,7 +3150,7 @@ tasks.register("verifyShopCsvBossGates") { "pack/migrations/26.2.0-alpha.194-shop-csv-boss-gates-to-alpha.195.json")) def provider = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopCatalogProvider.java").text def catalog = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.pack_version != "26.2.0-alpha.195" || migration.target?.modules?.sanctuary != "0.0.0-alpha.111" || migration.compatibility?.sanctuary_data_version != 11 @@ -3181,7 +3181,7 @@ tasks.register("verifyDeliveryBoxOrientation") { def blockstate = new JsonSlurper().parse( file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json")) def expectedVariants = ["facing=north", "facing=east", "facing=south", "facing=west"].toSet() - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.modules?.sanctuary != "0.0.0-alpha.112" || migration.mailbox?.id != "sanctuary:delivery_box" || migration.mailbox?.horizontal_facing != true @@ -3209,7 +3209,7 @@ tasks.register("verifyInventoryShiftClick") { def mixin = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/AbstractContainerMenuMixin.java").text def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AbstractContainerScreenMixin.java").text def gui = file("INVENTORY_GUI.md").text - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || !transfer.contains("shouldAppendOverflow") || !transfer.contains("isVanillaStorageRange") || !transfer.contains("hotbarStart") @@ -3255,7 +3255,7 @@ tasks.register("verifyQuestRequests") { new JsonSlurper().parse(file("src/main/resources/data/sanctuary/loot_table/blocks/${path}.json")) .pools[0].entries[0].name } - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.modules?.sanctuary != "0.0.0-alpha.113" || migration.quest_requests?.save_key != "quest_requests" || migration.quest_requests?.data_version != 1 @@ -3313,7 +3313,7 @@ tasks.register("verifyQuestRequestContracts") { def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale -> [(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))] } - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.198" || migration.source?.modules?.sanctuary != "0.0.0-alpha.114" || migration.target?.pack_version != "26.2.0-alpha.199" @@ -3382,7 +3382,7 @@ tasks.register("verifyQuestRequestEscrow") { def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale -> [(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))] } - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.source?.pack_version != "26.2.0-alpha.200" || migration.source?.modules?.sanctuary != "0.0.0-alpha.115" || migration.target?.pack_version != "26.2.0-alpha.201" @@ -3461,7 +3461,7 @@ tasks.register("verifyCraftableBannerCape") { def locales = ["fr_fr", "en_us", "ru_ru"].collect { new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json")) } - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.modules?.sanctuary != "0.0.0-alpha.130" || migration.white_cape?.crafting_material_count != 6 || migration.white_cape?.maximum_pattern_layers != 6 @@ -3516,7 +3516,7 @@ tasks.register("verifyAlphaIndev") { def locales = ["fr_fr", "en_us", "ru_ru"].collect { new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json")) } - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.target?.modules?.sanctuary != "0.0.0-alpha.122" || migration.alpha_world?.dimension != "sanctuary:alpha" || migration.alpha_world?.legacy_dimension != "portals:alpha" @@ -3550,12 +3550,15 @@ tasks.register("verifyBackroom") { file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"), file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java"), file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java"), + file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java"), file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"), file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"), file("src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json")) doLast { def migration = new JsonSlurper().parse(rootProject.file("pack/migrations/sanctuary-backroom-data-v14.json")) def dimensionType = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/dimension_type/backroom.json")) + def noiseSettings = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/worldgen/noise_settings/backroom.json")) + def biome = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/worldgen/biome/backroom.json")) def presets = [ file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"), file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"), @@ -3567,12 +3570,13 @@ tasks.register("verifyBackroom") { def chest = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomChestService.java").text def mailbox = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java").text def architecture = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java").text + def layout = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java").text def materialApi = file( "src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text def locales = ["fr_fr", "en_us", "ru_ru"].collect { new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json")) } - if (project.version.toString() != "0.0.0-alpha.131" + if (project.version.toString() != "0.0.0-alpha.132" || migration.source_data_version != 13 || migration.target_data_version != 14 || migration.integration_modules?.anotherworld?.source != "0.0.0-alpha.56" || migration.integration_modules?.anotherworld?.target != "0.0.0-alpha.57" @@ -3580,6 +3584,12 @@ tasks.register("verifyBackroom") { || presets.any { it.dimensions?.get("sanctuary:backroom") == null } || dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_set_spawn != "never" || dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_sleep != "never" + || dimensionType.min_y != -64 || dimensionType.height != 448 || dimensionType.logical_height != 448 + || dimensionType.ambient_light != 0.0 || dimensionType.has_ceiling != true + || dimensionType.attributes?.get("minecraft:visual/fog_color") != "#000000" + || noiseSettings.noise?.min_y != -64 || noiseSettings.noise?.height != 448 + || noiseSettings.disable_mob_generation != true + || biome.creature_spawn_probability != 0.0 || !biome.spawners?.monster?.isEmpty() || !data.contains("MAX_LOST_ITEMS = 16_384") || !data.contains("MAX_CHEST_STATES = 65_536") || !data.contains("PERSONAL_RESERVATION_TICKS = 24_000L") || !data.contains("indexOfLoss(marker)") || !data.contains("BACKROOM_LOSS_COMPONENT") @@ -3587,17 +3597,21 @@ tasks.register("verifyBackroom") { || !service.contains("ARRIVAL_RADIUS = 2_000") || !service.contains("insertTransaction") || !service.contains("AUTHORIZED_TRANSITIONS") || !service.contains("level -> !isBackroom(level)") + || !service.contains("MobCategory.MONSTER") || !service.contains("Blocks.TORCH") || !mailbox.contains("MAX_PROCESSED_TRANSACTIONS = 128") || !chest.contains("REFRESH_TICKS = 12_000L") || !chest.contains("ordinal == 3") || !architecture.contains("PLAYABLE_RADIUS = 2_000") || !architecture.contains("BackroomMaterialApi.anomalyMaterial()") + || !layout.contains("WORLD_MIN_Y = -64") || !layout.contains("WORLD_MAX_Y_EXCLUSIVE = 384") + || !layout.contains("LEVEL_0(295, 383)") || !layout.contains("LEVEL_4(-64, 27)") + || !layout.contains("mazeConnected") || !layout.contains("RoomCorner") || !materialApi.contains("registerAnomalyMaterial") || locales.any { !it.containsKey("dimension.sanctuary.backroom") || !it.containsKey("message.sanctuary.backroom.bed_entry_only") || !it.containsKey("message.sanctuary.backroom.registry_busy") || !it.containsKey("message.sanctuary.backroom.mailbox_full") || !it.containsKey("message.sanctuary.backroom.exited_with_losses") }) { - throw new GradleException("Sanctuary alpha.131 Backroom contract is incomplete") + throw new GradleException("Sanctuary alpha.132 Backroom contract is incomplete") } } } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomService.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomService.java index 9e2b256..d72e8ce 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomService.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomService.java @@ -3,12 +3,15 @@ package fr.koka99cab.sanctuary26.sanctuary.backroom; import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod; import fr.koka99cab.sanctuary26.sanctuary.shop.ShopData; import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions; +import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature; +import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.commands.CommandSourceStack; @@ -18,8 +21,9 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.MobCategory; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.portal.TeleportTransition; import net.minecraft.world.phys.Vec3; @@ -36,6 +40,9 @@ public final class BackroomService { if (initialized) return; initialized = true; ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> reconcile(handler.player)); + ServerEntityEvents.ENTITY_LOAD.register((entity, level) -> { + if (isBackroom(level) && entity.getType().getCategory() == MobCategory.MONSTER) entity.discard(); + }); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { dispatcher.register(Commands.literal("backroom") .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) @@ -101,6 +108,7 @@ public final class BackroomService { data.finishEntry(player.getUUID()); BlockPos arrival = chooseArrival(destination, player.getUUID(), data); + placeArrivalTorch(destination, arrival); data.beginExpedition(player.getUUID(), arrival.asLong()); TeleportTransition transition = new TeleportTransition(destination, Vec3.atBottomCenterOf(arrival), Vec3.ZERO, player.getYRot(), player.getXRot(), @@ -166,15 +174,14 @@ public final class BackroomService { private static BlockPos chooseArrival(ServerLevel level, UUID player, BackroomData data) { RandomSource random = level.getRandom(); List recent = data.recentArrivals(player); + int arrivalY = BackroomLayout.ENTRY_FLOOR_Y + 1; BlockPos best = null; for (int attempt = 0; attempt < ARRIVAL_ATTEMPTS; attempt++) { double angle = random.nextDouble() * Math.PI * 2.0D; double radius = Math.sqrt(random.nextDouble()) * ARRIVAL_RADIUS; int x = Mth.floor(Math.cos(angle) * radius); int z = Mth.floor(Math.sin(angle) * radius); - int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z), - level.getMinY() + 2, level.getMaxY() - 3); - BlockPos candidate = new BlockPos(x, y, z); + BlockPos candidate = new BlockPos(x, arrivalY, z); best = candidate; if (farFromRecent(candidate, recent) && safe(level, candidate)) return candidate; } @@ -182,9 +189,7 @@ public final class BackroomService { if (fallback != null) return fallback; fallback = findSafeNear(level, BlockPos.ZERO, 48); if (fallback != null) return fallback; - int originY = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, 0, 0), - level.getMinY() + 2, level.getMaxY() - 3); - return new BlockPos(0, originY, 0); + return new BlockPos(8, arrivalY, 8); } private static boolean farFromRecent(BlockPos candidate, List recent) { @@ -207,15 +212,32 @@ public final class BackroomService { if (ring > 0 && Math.abs(dx) != ring && Math.abs(dz) != ring) continue; int x = center.getX() + dx, z = center.getZ() + dz; if ((long) x * x + (long) z * z > (long) ARRIVAL_RADIUS * ARRIVAL_RADIUS) continue; - int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z), - level.getMinY() + 2, level.getMaxY() - 3); - BlockPos candidate = new BlockPos(x, y, z); + BlockPos candidate = new BlockPos(x, BackroomLayout.ENTRY_FLOOR_Y + 1, z); if (safe(level, candidate)) return candidate; } } return null; } + private static void placeArrivalTorch(ServerLevel level, BlockPos arrival) { + level.setBlock(arrival.below(), Blocks.BIRCH_PLANKS.defaultBlockState(), 3); + level.setBlock(arrival, Blocks.AIR.defaultBlockState(), 3); + level.setBlock(arrival.above(), Blocks.AIR.defaultBlockState(), 3); + for (var direction : net.minecraft.core.Direction.Plane.HORIZONTAL) { + BlockPos torch = arrival.relative(direction); + if (!BackroomArchitectureFeature.withinPlayableArea(torch.getX(), torch.getZ())) continue; + if (level.getBlockState(torch).isAir() + && level.getBlockState(torch.below()).isCollisionShapeFullBlock(level, torch.below())) { + level.setBlock(torch, Blocks.TORCH.defaultBlockState(), 3); + return; + } + } + BlockPos torch = arrival.east(); + if (!BackroomArchitectureFeature.withinPlayableArea(torch.getX(), torch.getZ())) torch = arrival.west(); + level.setBlock(torch.below(), Blocks.BIRCH_PLANKS.defaultBlockState(), 3); + level.setBlock(torch, Blocks.TORCH.defaultBlockState(), 3); + } + private static void notify(ServerPlayer player, String translationKey) { player.sendOverlayMessage(Component.translatable(translationKey)); } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java index fa3dc51..ca71237 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java @@ -1,8 +1,9 @@ package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature; import com.mojang.serialization.Codec; -import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod; import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi; +import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.BackroomLevel; +import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.RoomCorner; import java.util.Random; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -19,20 +20,42 @@ import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; /** - * Deterministic, chunk-local architecture for the persistent Backroom. - * Macro cells select authored palettes; randomness only varies plans inside a coherent family. + * Five deterministic Backroom levels stacked from Y=384 to Y=-64. Generation is chunk-local, + * while every wall, doorway, highway and hole is derived from world coordinates so chunk seams + * remain open and coherent. */ public final class BackroomArchitectureFeature extends Feature { public static final int PLAYABLE_RADIUS = 2_000; private static final int CHUNK_SIZE = 16; - private static final int MACRO_CHUNKS = 8; + private static final int[] THRESHOLD_DECKS = {295, 313, 331, 349, 367}; + private static final int[] ARCHIVE_DECKS = {206, 228, 250, 272}; + private static final int[] UTILITY_DECKS = {28, 46, 64, 82, 100}; + private static final int[] LANDMARK_FLOORS = {349, 250, 117, 64, BackroomLayout.MAZE_FLOOR_Y}; + private static final BlockState CONCRETE_GRAY = Blocks.CONCRETE.pick(DyeColor.GRAY).defaultBlockState(); private static final BlockState CONCRETE_LIGHT = Blocks.CONCRETE.pick(DyeColor.LIGHT_GRAY).defaultBlockState(); private static final BlockState CONCRETE_WHITE = Blocks.CONCRETE.pick(DyeColor.WHITE).defaultBlockState(); + private static final BlockState GLASS_BLACK = Blocks.STAINED_GLASS.pick(DyeColor.BLACK).defaultBlockState(); + private static final BlockState GLASS_BROWN = Blocks.STAINED_GLASS.pick(DyeColor.BROWN).defaultBlockState(); private static final BlockState GLASS_CYAN = Blocks.STAINED_GLASS.pick(DyeColor.CYAN).defaultBlockState(); private static final BlockState CLAY_BROWN = Blocks.DYED_TERRACOTTA.pick(DyeColor.BROWN).defaultBlockState(); private static final BlockState CLAY_ORANGE = Blocks.DYED_TERRACOTTA.pick(DyeColor.ORANGE).defaultBlockState(); + private static final BlockState CLAY_YELLOW = Blocks.DYED_TERRACOTTA.pick(DyeColor.YELLOW).defaultBlockState(); private static final BlockState LEAVES = Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true); + private static final BlockState[] CORNER_MATERIALS = { + Blocks.BRICKS.defaultBlockState(), + Blocks.MUD_BRICKS.defaultBlockState(), + Blocks.STONE_BRICKS.defaultBlockState(), + CLAY_YELLOW, + Blocks.SMOOTH_SANDSTONE.defaultBlockState() + }; + private static final BlockState[] GLITCH_MATERIALS = { + Blocks.BRICKS.defaultBlockState(), + Blocks.MUD_BRICKS.defaultBlockState(), + Blocks.STONE_BRICKS.defaultBlockState(), + Blocks.CRACKED_STONE_BRICKS.defaultBlockState(), + CLAY_BROWN + }; public BackroomArchitectureFeature(Codec codec) { super(codec); @@ -47,17 +70,15 @@ public final class BackroomArchitectureFeature extends Feature city(level, startX, startZ, random); - case OFFICES -> offices(level, startX, startZ, random); - case PLAINS -> plains(level, startX, startZ, random); - case AGGREGATES -> aggregates(level, startX, startZ, random); - case ROTATED -> rotated(level, startX, startZ, random); - } - placeLootLandmarks(level, startX, startZ, random, architecture); + long worldSeed = level.getSeed(); + Random random = new Random(fingerprint(worldSeed, chunkX, chunkZ)); + buildLevelShells(level, worldSeed, startX, startZ); + buildThreshold(level, worldSeed, startX, startZ); + buildArchives(level, worldSeed, startX, startZ); + buildCity(level, worldSeed, startX, startZ, chunkX, chunkZ); + buildUtilities(level, worldSeed, startX, startZ); + buildGlitchMaze(level, worldSeed, startX, startZ); + placeLootLandmarks(level, startX, startZ, random); placeRareAnomaly(level, startX, startZ, random); return true; } @@ -66,161 +87,387 @@ public final class BackroomArchitectureFeature extends Feature Blocks.BIRCH_PLANKS.defaultBlockState(); + case LEVEL_1 -> Blocks.DARK_OAK_PLANKS.defaultBlockState(); + case LEVEL_2 -> CONCRETE_GRAY; + case LEVEL_3 -> Blocks.POLISHED_ANDESITE.defaultBlockState(); + case LEVEL_4 -> Blocks.STONE_BRICKS.defaultBlockState(); + }; + } + + private static BlockState ceilingMaterial(BackroomLevel zone) { + return switch (zone) { + case LEVEL_0 -> CLAY_YELLOW; + case LEVEL_1 -> CONCRETE_LIGHT; + case LEVEL_2 -> CONCRETE_GRAY; + case LEVEL_3 -> Blocks.BRICKS.defaultBlockState(); + case LEVEL_4 -> Blocks.MUD_BRICKS.defaultBlockState(); + }; + } + + private static void buildThreshold(WorldGenLevel level, long seed, int x0, int z0) { + int cellX = Math.floorDiv(x0, CHUNK_SIZE); + int cellZ = Math.floorDiv(z0, CHUNK_SIZE); + for (int deck = 0; deck < THRESHOLD_DECKS.length; deck++) { + int floorY = THRESHOLD_DECKS[deck]; + int roofY = deck + 1 < THRESHOLD_DECKS.length ? THRESHOLD_DECKS[deck + 1] + : BackroomLevel.LEVEL_0.ceilingY(); + if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.BIRCH_PLANKS.defaultBlockState()); + int pattern = BackroomLayout.roomPattern(seed, BackroomLevel.LEVEL_0, cellX, cellZ, deck); + BlockState wall = pattern % 2 == 0 ? CLAY_YELLOW : Blocks.SMOOTH_SANDSTONE.defaultBlockState(); + for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) { + int localX = Math.floorMod(x, CHUNK_SIZE), localZ = Math.floorMod(z, CHUNK_SIZE); + boolean boundaryX = localX == 0, boundaryZ = localZ == 0; + if (!boundaryX && !boundaryZ) continue; + boolean doorway = boundaryX != boundaryZ && doorway(boundaryX ? localZ : localX, 7, 9); + if (doorway) continue; + for (int y = floorY + 1; y < roofY; y++) set(level, x, y, z, wall); + } + placeThresholdPattern(level, x0, floorY, z0, roofY, pattern, wall); + for (RoomCorner corner : RoomCorner.values()) { + placeCorner(level, seed, x0, floorY, z0, cellX, cellZ, deck, corner); + } + } + } + + private static void placeThresholdPattern(WorldGenLevel level, int x0, int floorY, int z0, + int roofY, int pattern, BlockState wall) { + switch (pattern) { + case 1 -> partitionX(level, x0 + 5, floorY, roofY, z0 + 2, z0 + 13, z0 + 8, wall, false); + case 2 -> partitionZ(level, z0 + 10, floorY, roofY, x0 + 2, x0 + 13, x0 + 8, wall, false); + case 3 -> { + partitionX(level, x0 + 5, floorY, roofY, z0 + 2, z0 + 11, z0 + 7, wall, false); + partitionZ(level, z0 + 10, floorY, roofY, x0 + 5, x0 + 13, x0 + 10, wall, false); + } + case 4 -> partitionX(level, x0 + 8, floorY, roofY, z0 + 2, z0 + 13, z0 + 8, wall, true); + case 5 -> { + for (int x : new int[]{x0 + 5, x0 + 10}) for (int z : new int[]{z0 + 5, z0 + 10}) { + for (int y = floorY + 1; y <= Math.min(floorY + 6, roofY - 1); y++) { + set(level, x, y, z, y % 2 == 0 ? CLAY_ORANGE : CLAY_BROWN); + } } } - if (random.nextInt(3) == 0) { - int deskX = x0 + 5 + random.nextInt(6), deskZ = z0 + 5 + random.nextInt(6); - set(level, deskX, baseY + 1, deskZ, Blocks.OAK_SLAB.defaultBlockState()); - set(level, deskX + 1, baseY + 1, deskZ, Blocks.BOOKSHELF.defaultBlockState()); - set(level, deskX, baseY + 1, deskZ + 1, Blocks.CARPET.pick(DyeColor.BROWN).defaultBlockState()); - } + default -> { } } } - private static void plains(WorldGenLevel level, int x0, int z0, Random random) { - int baseY = 68 + random.nextInt(3); - for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) { - set(level, x, baseY - 2, z, Blocks.STONE.defaultBlockState()); - set(level, x, baseY - 1, z, Blocks.DIRT.defaultBlockState()); - set(level, x, baseY, z, Blocks.GRASS_BLOCK.defaultBlockState()); - } - if (random.nextInt(3) == 0) tree(level, x0 + 4 + random.nextInt(8), baseY + 1, z0 + 4 + random.nextInt(8)); - if (random.nextInt(7) == 0) { - for (int x = x0 + 3; x <= x0 + 11; x++) for (int z = z0 + 3; z <= z0 + 11; z++) { - boolean edge = x == x0 + 3 || x == x0 + 11 || z == z0 + 3 || z == z0 + 11; - if (edge) for (int y = baseY + 1; y <= baseY + 4; y++) set(level, x, y, z, Blocks.MUD_BRICKS.defaultBlockState()); - } - } - } - - private static void aggregates(WorldGenLevel level, int x0, int z0, Random random) { - int baseY = 74 + random.nextInt(10); - platform(level, x0, z0, baseY, CONCRETE_LIGHT); - boxRoom(level, x0 + 2, baseY + 1, z0 + 2, x0 + 12, baseY + 7, z0 + 12, - Blocks.SMOOTH_STONE.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState()); - if (random.nextBoolean()) { - int upper = baseY + 20 + random.nextInt(12); - platform(level, x0 + 4, z0 + 4, upper, CLAY_ORANGE); - for (int y = baseY + 1; y <= upper; y++) set(level, x0 + 8, y, z0 + 8, Blocks.IRON_CHAIN.defaultBlockState()); - } - } - - private static void rotated(WorldGenLevel level, int x0, int z0, Random random) { - int y0 = 70 + random.nextInt(12); - for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y0, z, CONCRETE_LIGHT); - boolean alongX = random.nextBoolean(); - for (int depth = 2; depth <= 13; depth++) for (int y = y0 + 1; y <= y0 + 13; y++) { - int x = alongX ? x0 + depth : x0 + 3; - int z = alongX ? z0 + 3 : z0 + depth; - BlockState state = (y == y0 + 1 || y == y0 + 13 || depth == 2 || depth == 13) - ? CLAY_BROWN : Blocks.AIR.defaultBlockState(); - set(level, x, y, z, state); - } - // Furniture fixed to the vertical "floor" sells the rotated-room illusion without changing gravity. - for (int offset = 4; offset <= 10; offset += 3) { - int x = alongX ? x0 + offset : x0 + 4; - int z = alongX ? z0 + 4 : z0 + offset; - set(level, x, y0 + 4, z, Blocks.BOOKSHELF.defaultBlockState()); - } - } - - private static void placeLootLandmarks(WorldGenLevel level, int x0, int z0, Random random, Architecture architecture) { - int y = switch (architecture) { - case CITY -> 65; - case OFFICES -> 73; - case PLAINS -> 72; - case AGGREGATES -> 83; - case ROTATED -> 84; + private static void placeCorner(WorldGenLevel level, long seed, int x0, int floorY, int z0, + int cellX, int cellZ, int deck, RoomCorner corner) { + int x = switch (corner) { + case NORTH_WEST, SOUTH_WEST -> x0 + 2; + case NORTH_EAST, SOUTH_EAST -> x0 + 13; }; - int x = x0 + 7, z = z0 + 7; - while (!level.getBlockState(new BlockPos(x, y, z)).isAir() && y < 180) y++; - while (!level.getBlockState(new BlockPos(x, y - 1, z)).isCollisionShapeFullBlock(level, new BlockPos(x, y - 1, z)) && y > 2) y--; - if (random.nextInt(5) == 0) set(level, x, y, z, Blocks.CHEST.defaultBlockState()); - if (random.nextInt(17) == 0) placeBed(level, x0 + 9, y, z0 + 8); + int z = switch (corner) { + case NORTH_WEST, NORTH_EAST -> z0 + 2; + case SOUTH_WEST, SOUTH_EAST -> z0 + 13; + }; + BlockState material = CORNER_MATERIALS[BackroomLayout.cornerVariant( + seed ^ (long) deck * 31L, BackroomLevel.LEVEL_0, cellX, cellZ, corner)]; + for (int y = floorY + 1; y <= floorY + 4; y++) set(level, x, y, z, material); + int towardX = corner == RoomCorner.NORTH_WEST || corner == RoomCorner.SOUTH_WEST ? 1 : -1; + int towardZ = corner == RoomCorner.NORTH_WEST || corner == RoomCorner.NORTH_EAST ? 1 : -1; + for (int distance = 1; distance <= 2; distance++) { + set(level, x + towardX * distance, floorY + 1, z, material); + set(level, x, floorY + 1, z + towardZ * distance, material); + } + } + + private static void buildArchives(WorldGenLevel level, long seed, int x0, int z0) { + int macroX = Math.floorDiv(x0, 64), macroZ = Math.floorDiv(z0, 64); + for (int deck = 0; deck < ARCHIVE_DECKS.length; deck++) { + int floorY = ARCHIVE_DECKS[deck]; + int roofY = deck + 1 < ARCHIVE_DECKS.length ? ARCHIVE_DECKS[deck + 1] + : BackroomLevel.LEVEL_1.ceilingY(); + if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.DARK_OAK_PLANKS.defaultBlockState()); + boolean library = Math.floorMod((int) fingerprint(seed ^ deck, macroX, macroZ), 3) != 0; + for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) { + int localX = Math.floorMod(x, 32), localZ = Math.floorMod(z, 32); + boolean wallX = localX == 0, wallZ = localZ == 0; + if (!wallX && !wallZ) continue; + boolean open = wallX != wallZ && doorway(wallX ? localZ : localX, 14, 18); + if (open) continue; + for (int y = floorY + 1; y < roofY; y++) { + BlockState state = y >= floorY + 5 && y <= floorY + 9 ? GLASS_BROWN : CONCRETE_LIGHT; + set(level, x, y, z, state); + } + } + if (library) buildLibraryRoom(level, x0, floorY, z0, roofY); + else buildOfficeRoom(level, seed, x0, floorY, z0, roofY, deck); + } + } + + private static void buildLibraryRoom(WorldGenLevel level, int x0, int floorY, int z0, int roofY) { + for (int x = x0 + 1; x < x0 + 15; x++) for (int z = z0 + 1; z < z0 + 15; z++) { + int roomX = Math.floorMod(x, 32), roomZ = Math.floorMod(z, 32); + boolean shelf = (Math.floorMod(x, 8) == 3 || Math.floorMod(x, 8) == 4) + && roomX > 3 && roomX < 28 && (roomZ < 12 || roomZ > 20); + if (!shelf) continue; + for (int y = floorY + 1; y <= Math.min(floorY + 6, roofY - 1); y++) { + set(level, x, y, z, Blocks.BOOKSHELF.defaultBlockState()); + } + } + if (Math.floorMod(x0 + z0 + floorY, 3) == 0) { + for (int x = x0 + 6; x <= x0 + 9; x++) set(level, x, floorY + 1, z0 + 8, + Blocks.OAK_SLAB.defaultBlockState()); + } + } + + private static void buildOfficeRoom(WorldGenLevel level, long seed, int x0, int floorY, int z0, + int roofY, int deck) { + boolean alongX = (fingerprint(seed, Math.floorDiv(x0, 16), Math.floorDiv(z0, 16)) + deck & 1L) == 0L; + if (alongX) partitionX(level, x0 + 8, floorY, roofY, z0 + 1, z0 + 14, z0 + 8, CONCRETE_WHITE, true); + else partitionZ(level, z0 + 8, floorY, roofY, x0 + 1, x0 + 14, x0 + 8, CONCRETE_WHITE, true); + set(level, x0 + 4, floorY + 1, z0 + 4, Blocks.OAK_SLAB.defaultBlockState()); + set(level, x0 + 5, floorY + 1, z0 + 4, Blocks.BOOKSHELF.defaultBlockState()); + set(level, x0 + 11, floorY + 1, z0 + 11, Blocks.OAK_SLAB.defaultBlockState()); + } + + private static void buildCity(WorldGenLevel level, long seed, int x0, int z0, int chunkX, int chunkZ) { + boolean roadFree = true; + for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) { + if (BackroomLayout.transitionHole(seed, BackroomLevel.LEVEL_2.ordinal(), x, z)) continue; + boolean highway = BackroomLayout.cityHighway(x, z); + if (highway) roadFree = false; + BlockState surface = highway ? CONCRETE_GRAY : CONCRETE_LIGHT; + if (highway && (Math.floorMod(x, 8) == 0 || Math.floorMod(z, 8) == 0)) surface = CONCRETE_WHITE; + set(level, x, BackroomLevel.LEVEL_2.floorY(), z, surface); + } + boolean park = roadFree && BackroomLayout.cityPark(seed, chunkX, chunkZ); + if (park) { + buildPark(level, seed, x0, z0); + return; + } + if (!roadFree) return; + + Random random = new Random(fingerprint(seed ^ 0x514e7a2c39d8b60fL, chunkX, chunkZ)); + int floorY = BackroomLevel.LEVEL_2.floorY(); + int roofY = floorY + 24 + random.nextInt(56); + int minX = x0 + 1, maxX = x0 + 14, minZ = z0 + 1, maxZ = z0 + 14; + BlockState frame = random.nextBoolean() ? CONCRETE_LIGHT : Blocks.STONE_BRICKS.defaultBlockState(); + BlockState glass = random.nextBoolean() ? GLASS_CYAN : GLASS_BLACK; + for (int y = floorY + 1; y <= roofY; y++) { + boolean slab = y == roofY || Math.floorMod(y - floorY, 7) == 0; + for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) { + boolean edge = x == minX || x == maxX || z == minZ || z == maxZ; + boolean entrance = y <= floorY + 4 && (((x == minX || x == maxX) && doorway(z - z0, 7, 9)) + || ((z == minZ || z == maxZ) && doorway(x - x0, 7, 9))); + if (entrance) continue; + if (slab) set(level, x, y, z, frame); + else if (edge) set(level, x, y, z, Math.floorMod(y - floorY, 7) <= 2 ? frame : glass); + } + } + } + + private static void buildPark(WorldGenLevel level, long seed, int x0, int z0) { + int floorY = BackroomLevel.LEVEL_2.floorY(); + for (int x = x0 + 1; x < x0 + 15; x++) for (int z = z0 + 1; z < z0 + 15; z++) { + if (BackroomLayout.transitionHole(seed, BackroomLevel.LEVEL_2.ordinal(), x, z)) continue; + BlockState surface = x == x0 + 8 || z == z0 + 8 + ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.GRASS_BLOCK.defaultBlockState(); + set(level, x, floorY, z, surface); + } + Random random = new Random(fingerprint(seed ^ 0x1974acb65d203e8fL, + Math.floorDiv(x0, 16), Math.floorDiv(z0, 16))); + int trees = 1 + random.nextInt(2); + for (int index = 0; index < trees; index++) { + tree(level, x0 + 4 + random.nextInt(8), floorY + 1, z0 + 4 + random.nextInt(8)); + } + } + + private static void buildUtilities(WorldGenLevel level, long seed, int x0, int z0) { + for (int deck = 0; deck < UTILITY_DECKS.length; deck++) { + int floorY = UTILITY_DECKS[deck]; + int roofY = deck + 1 < UTILITY_DECKS.length ? UTILITY_DECKS[deck + 1] + : BackroomLevel.LEVEL_3.ceilingY(); + if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.POLISHED_ANDESITE.defaultBlockState()); + for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) { + int localX = Math.floorMod(x, 24), localZ = Math.floorMod(z, 24); + boolean wallX = localX == 0, wallZ = localZ == 0; + if (wallX || wallZ) { + boolean open = wallX != wallZ && doorway(wallX ? localZ : localX, 10, 14); + if (!open) for (int y = floorY + 1; y < roofY; y++) { + BlockState state = y == floorY + 8 ? Blocks.IRON_BARS.defaultBlockState() + : (deck + Math.floorDiv(x + z, 24)) % 2 == 0 + ? Blocks.BRICKS.defaultBlockState() : Blocks.MUD_BRICKS.defaultBlockState(); + set(level, x, y, z, state); + } + } + boolean overheadGrid = floorY + 12 < roofY + && ((localX == 4 && localZ >= 7 && localZ <= 17) + || (localZ == 4 && localX >= 7 && localX <= 17)); + if (overheadGrid) set(level, x, floorY + 12, z, Blocks.IRON_BARS.defaultBlockState()); + } + } + } + + private static void buildGlitchMaze(WorldGenLevel level, long seed, int x0, int z0) { + for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) { + if (!BackroomLayout.deckHole(seed, BackroomLayout.MAZE_FLOOR_Y, x, z)) { + set(level, x, BackroomLayout.MAZE_FLOOR_Y, z, Blocks.STONE_BRICKS.defaultBlockState()); + } + if (!BackroomLayout.mazeRoofHole(seed, x, z)) { + set(level, x, BackroomLayout.MAZE_ROOF_Y, z, Blocks.MUD_BRICKS.defaultBlockState()); + } + for (int y = BackroomLevel.LEVEL_4.floorY() + 1; y < BackroomLevel.LEVEL_4.ceilingY(); y++) { + if (y >= BackroomLayout.MAZE_FLOOR_Y && y <= BackroomLayout.MAZE_ROOF_Y) continue; + boolean edgeX = Math.floorMod(x, 8) == 0; + boolean edgeY = Math.floorMod(y - BackroomLevel.LEVEL_4.floorY(), 8) == 0; + boolean edgeZ = Math.floorMod(z, 8) == 0; + if ((edgeX && edgeY) || (edgeX && edgeZ) || (edgeY && edgeZ)) { + set(level, x, y, z, glitchMaterial(seed, x, y, z)); + } + } + } + + for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) { + boolean wallX = Math.floorMod(x, CHUNK_SIZE) == 0; + boolean wallZ = Math.floorMod(z, CHUNK_SIZE) == 0; + if (!wallX && !wallZ) continue; + boolean open = false; + if (wallX != wallZ) { + if (wallX) { + int rightX = Math.floorDiv(x, CHUNK_SIZE), cellZ = Math.floorDiv(z, CHUNK_SIZE); + boolean connected = BackroomLayout.mazeConnected(seed, rightX - 1, cellZ, rightX, cellZ); + boolean merged = BackroomLayout.mergedMazeRooms(seed, rightX - 1, cellZ, rightX, cellZ); + open = merged || connected && doorway(Math.floorMod(z, CHUNK_SIZE), 7, 9); + } else { + int cellX = Math.floorDiv(x, CHUNK_SIZE), southZ = Math.floorDiv(z, CHUNK_SIZE); + boolean connected = BackroomLayout.mazeConnected(seed, cellX, southZ - 1, cellX, southZ); + boolean merged = BackroomLayout.mergedMazeRooms(seed, cellX, southZ - 1, cellX, southZ); + open = merged || connected && doorway(Math.floorMod(x, CHUNK_SIZE), 7, 9); + } + } + if (open) continue; + for (int y = BackroomLayout.MAZE_FLOOR_Y + 1; y < BackroomLayout.MAZE_ROOF_Y; y++) { + BlockState state = y >= BackroomLayout.MAZE_FLOOR_Y + 4 + && y <= BackroomLayout.MAZE_FLOOR_Y + 6 ? GLASS_BLACK : glitchMaterial(seed, x, y, z); + set(level, x, y, z, state); + } + } + + int cellX = Math.floorDiv(x0, CHUNK_SIZE), cellZ = Math.floorDiv(z0, CHUNK_SIZE); + int pattern = BackroomLayout.roomPattern(seed, BackroomLevel.LEVEL_4, cellX, cellZ, 0); + BlockState wall = GLITCH_MATERIALS[Math.floorMod(pattern, GLITCH_MATERIALS.length)]; + placeThresholdPattern(level, x0, BackroomLayout.MAZE_FLOOR_Y, z0, + BackroomLayout.MAZE_ROOF_Y, pattern, wall); + } + + private static BlockState glitchMaterial(long seed, int x, int y, int z) { + int index = Math.floorMod((int) fingerprint(seed ^ y, Math.floorDiv(x, 8), Math.floorDiv(z, 8)), + GLITCH_MATERIALS.length); + return GLITCH_MATERIALS[index]; + } + + private static void fillDeck(WorldGenLevel level, long seed, int floorY, int x0, int z0, + BlockState state) { + for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) { + if (!BackroomLayout.deckHole(seed, floorY, x, z)) set(level, x, floorY, z, state); + } + } + + private static void partitionX(WorldGenLevel level, int x, int floorY, int roofY, + int minZ, int maxZ, int doorZ, BlockState wall, boolean window) { + for (int z = minZ; z <= maxZ; z++) { + if (Math.abs(z - doorZ) <= 1) continue; + for (int y = floorY + 1; y < roofY; y++) { + set(level, x, y, z, window && y >= floorY + 5 && y <= floorY + 8 ? GLASS_BROWN : wall); + } + } + } + + private static void partitionZ(WorldGenLevel level, int z, int floorY, int roofY, + int minX, int maxX, int doorX, BlockState wall, boolean window) { + for (int x = minX; x <= maxX; x++) { + if (Math.abs(x - doorX) <= 1) continue; + for (int y = floorY + 1; y < roofY; y++) { + set(level, x, y, z, window && y >= floorY + 5 && y <= floorY + 8 ? GLASS_BROWN : wall); + } + } + } + + private static boolean doorway(int coordinate, int minimum, int maximum) { + return coordinate >= minimum && coordinate <= maximum; + } + + private static void placeLootLandmarks(WorldGenLevel level, int x0, int z0, Random random) { + if (random.nextInt(5) == 0) { + int floorY = LANDMARK_FLOORS[random.nextInt(LANDMARK_FLOORS.length)]; + int x = x0 + 7, y = floorY + 1, z = z0 + 7; + set(level, x, floorY, z, floorMaterial(BackroomLayout.levelAt(floorY))); + set(level, x, y, z, Blocks.CHEST.defaultBlockState()); + set(level, x, y + 1, z, Blocks.AIR.defaultBlockState()); + } + if (random.nextInt(96) == 0) { + int floorY = 272; + int x = x0 + 7, z = z0 + 8; + set(level, x, floorY, z, Blocks.DARK_OAK_PLANKS.defaultBlockState()); + set(level, x + 1, floorY, z, Blocks.DARK_OAK_PLANKS.defaultBlockState()); + for (int dx = 0; dx <= 1; dx++) for (int dy = 1; dy <= 2; dy++) { + set(level, x + dx, floorY + dy, z, Blocks.AIR.defaultBlockState()); + } + placeBed(level, x, floorY + 1, z); + } } private static void placeRareAnomaly(WorldGenLevel level, int x0, int z0, Random random) { if (random.nextInt(96) == 0) { Block anomaly = BackroomMaterialApi.anomalyMaterial(); - for (int i = 0; i < 5; i++) set(level, x0 + 5 + random.nextInt(6), 66 + random.nextInt(20), - z0 + 5 + random.nextInt(6), anomaly.defaultBlockState()); + int floorY = LANDMARK_FLOORS[random.nextInt(LANDMARK_FLOORS.length)]; + for (int index = 0; index < 5; index++) { + set(level, x0 + 5 + random.nextInt(6), floorY + 1 + random.nextInt(4), + z0 + 5 + random.nextInt(6), anomaly.defaultBlockState()); + } } if (random.nextInt(4_096) == 0 && withinPlayableArea(x0 + 7, z0 + 7) && withinPlayableArea(x0 + 9, z0 + 9)) { for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) for (int dz = -1; dz <= 1; dz++) { - set(level, x0 + 8 + dx, 40 + dy, z0 + 8 + dz, Blocks.SMOOTH_STONE.defaultBlockState()); + set(level, x0 + 8 + dx, -40 + dy, z0 + 8 + dz, Blocks.SMOOTH_STONE.defaultBlockState()); } - set(level, x0 + 8, 40, z0 + 8, Blocks.DIAMOND_BLOCK.defaultBlockState()); + set(level, x0 + 8, -40, z0 + 8, Blocks.DIAMOND_BLOCK.defaultBlockState()); } } private static void placeBed(WorldGenLevel level, int x, int y, int z) { if (!withinPlayableArea(x, z) || !withinPlayableArea(x + 1, z)) return; Block bed = Blocks.BED.pick(DyeColor.WHITE); - BlockState foot = bed.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST).setValue(BedBlock.PART, BedPart.FOOT); + BlockState foot = bed.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST) + .setValue(BedBlock.PART, BedPart.FOOT); BlockState head = foot.setValue(BedBlock.PART, BedPart.HEAD); set(level, x, y, z, foot); set(level, x + 1, y, z, head); } - private static void platform(WorldGenLevel level, int x0, int z0, int y, BlockState state) { - for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y, z, state); - } - - private static void boxRoom(WorldGenLevel level, int minX, int minY, int minZ, - int maxX, int maxY, int maxZ, BlockState shell, BlockState floor) { - for (int x = minX; x <= maxX; x++) for (int y = minY; y <= maxY; y++) for (int z = minZ; z <= maxZ; z++) { - boolean boundary = x == minX || x == maxX || y == minY || y == maxY || z == minZ || z == maxZ; - if (boundary) set(level, x, y, z, y == minY ? floor : shell); - } - } - private static void tree(WorldGenLevel level, int x, int y, int z) { - for (int dy = 0; dy < 5; dy++) set(level, x, y + dy, z, Blocks.OAK_LOG.defaultBlockState()); - for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) for (int dy = 3; dy <= 6; dy++) { - if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 5) <= 5) set(level, x + dx, y + dy, z + dz, LEAVES); + for (int dy = 0; dy < 6; dy++) set(level, x, y + dy, z, Blocks.OAK_LOG.defaultBlockState()); + for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) for (int dy = 4; dy <= 7; dy++) { + if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 6) <= 5) set(level, x + dx, y + dy, z + dz, LEAVES); } } @@ -238,6 +485,4 @@ public final class BackroomArchitectureFeature extends Feature>> 31; } - - public enum Architecture { CITY, OFFICES, PLAINS, AGGREGATES, ROTATED } } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java new file mode 100644 index 0000000..177119d --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java @@ -0,0 +1,155 @@ +package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature; + +import java.util.Objects; + +/** + * Pure deterministic layout rules shared by Backroom generation, arrivals and executable tests. + * Level numbers run from the top down so a fall always moves toward the next numbered level. + */ +public final class BackroomLayout { + public static final int WORLD_MIN_Y = -64; + public static final int WORLD_MAX_Y_EXCLUSIVE = 384; + public static final int ENTRY_FLOOR_Y = 367; + public static final int ROOM_SIZE = 16; + public static final int MAZE_FLOOR_Y = 8; + public static final int MAZE_ROOF_Y = 18; + private static final int HOLE_CELL_SIZE = 64; + + private BackroomLayout() {} + + public static BackroomLevel levelAt(int y) { + for (BackroomLevel level : BackroomLevel.values()) { + if (y >= level.floorY && y <= level.ceilingY) return level; + } + throw new IllegalArgumentException("Y outside the Backroom: " + y); + } + + /** A matching opening through an upper floor and the lower level's ceiling. */ + public static boolean transitionHole(long seed, int transition, int x, int z) { + if (transition < 0 || transition >= BackroomLevel.values().length - 1) return false; + return authoredHole(seed ^ 0x2f53a71d4e6c890bL, transition, x, z, 1); + } + + /** Sparse holes between decks create vertical routes without turning every room into a shaft. */ + public static boolean deckHole(long seed, int floorY, int x, int z) { + return authoredHole(seed ^ 0x74c8e6219b0d35afL, floorY, x, z, 1); + } + + /** The last floor deliberately has occasional exits into the actual void. */ + public static boolean voidHole(long seed, int x, int z) { + return authoredHole(seed ^ 0x56de9c183af247b1L, 5, x, z, 2); + } + + /** Openings in the maze roof let a player who landed on it enter the generated rooms. */ + public static boolean mazeRoofHole(long seed, int x, int z) { + return authoredHole(seed ^ 0x6b8f1de425a390c7L, 6, x, z, 1); + } + + public static int roomPattern(long seed, BackroomLevel level, int cellX, int cellZ, int deck) { + return Math.floorMod((int) mix(seed ^ 0x3c79ac492ba7b653L, + cellX + level.ordinal() * 1_009, cellZ + deck * 4_099), 6); + } + + /** Each of the four authored corner directions independently reaches five material variants. */ + public static int cornerVariant(long seed, BackroomLevel level, int cellX, int cellZ, + RoomCorner corner) { + Objects.requireNonNull(level, "level"); + Objects.requireNonNull(corner, "corner"); + return Math.floorMod((int) mix(seed ^ 0x1c69b3f74ac4ae35L, + cellX + corner.ordinal() * 8_191, cellZ + level.ordinal() * 16_381), 5); + } + + /** + * Symmetric room-edge rule. Every maze cell has a parent one step nearer the origin, which makes + * the complete graph connected; extra deterministic edges introduce loops and glitches. + */ + public static boolean mazeConnected(long seed, int ax, int az, int bx, int bz) { + if (Math.abs(ax - bx) + Math.abs(az - bz) != 1) return false; + if (isParent(ax, az, bx, bz) || isParent(bx, bz, ax, az)) return true; + int minX = Math.min(ax, bx), minZ = Math.min(az, bz); + int edgeAxis = ax == bx ? 1 : 0; + return Math.floorMod((int) mix(seed ^ 0x5d42c713e9a86bf0L, + minX * 2 + edgeAxis, minZ * 2 + 1 - edgeAxis), 5) == 0; + } + + /** Some connected cells completely lose their partition and become rooms at a larger scale. */ + public static boolean mergedMazeRooms(long seed, int ax, int az, int bx, int bz) { + if (!mazeConnected(seed, ax, az, bx, bz)) return false; + int minX = Math.min(ax, bx), minZ = Math.min(az, bz); + int edgeAxis = ax == bx ? 1 : 0; + return Math.floorMod((int) mix(seed ^ 0x0b4e8f619c32d75aL, + minX * 2 + edgeAxis, minZ * 2 + 1 - edgeAxis), 11) == 0; + } + + /** Infinite orthogonal highways, nine blocks wide and 96 blocks apart. */ + public static boolean cityHighway(int x, int z) { + return centeredModulo(x - 8, 96) <= 4 || centeredModulo(z - 8, 96) <= 4; + } + + public static boolean cityPark(long seed, int chunkX, int chunkZ) { + return Math.floorMod((int) mix(seed ^ 0x43c2a1769f0e85bdL, chunkX, chunkZ), 11) == 0; + } + + private static boolean isParent(int childX, int childZ, int parentX, int parentZ) { + if (childX == 0 && childZ == 0) return false; + int expectedX = childX, expectedZ = childZ; + if (Math.abs(childX) >= Math.abs(childZ) && childX != 0) expectedX -= Integer.signum(childX); + else expectedZ -= Integer.signum(childZ); + return expectedX == parentX && expectedZ == parentZ; + } + + private static boolean authoredHole(long seed, int salt, int x, int z, int radius) { + int cellX = Math.floorDiv(x, HOLE_CELL_SIZE); + int cellZ = Math.floorDiv(z, HOLE_CELL_SIZE); + long hash = mix(seed ^ (long) salt * 0x9e3779b97f4a7c15L, cellX, cellZ); + int centerX = cellX * HOLE_CELL_SIZE + 8 + Math.floorMod((int) hash, HOLE_CELL_SIZE - 16); + int centerZ = cellZ * HOLE_CELL_SIZE + 8 + + Math.floorMod((int) (hash >>> 32), HOLE_CELL_SIZE - 16); + return Math.abs(x - centerX) <= radius && Math.abs(z - centerZ) <= radius; + } + + private static int centeredModulo(int value, int period) { + int wrapped = Math.floorMod(value + period / 2, period) - period / 2; + return Math.abs(wrapped); + } + + private static long mix(long seed, int x, int z) { + long value = seed ^ (long) x * 0x9E3779B97F4A7C15L ^ (long) z * 0xC2B2AE3D27D4EB4FL; + value ^= value >>> 30; + value *= 0xBF58476D1CE4E5B9L; + value ^= value >>> 27; + value *= 0x94D049BB133111EBL; + return value ^ value >>> 31; + } + + public enum BackroomLevel { + LEVEL_0(295, 383), + LEVEL_1(206, 294), + LEVEL_2(117, 205), + LEVEL_3(28, 116), + LEVEL_4(-64, 27); + + private final int floorY; + private final int ceilingY; + + BackroomLevel(int floorY, int ceilingY) { + this.floorY = floorY; + this.ceilingY = ceilingY; + } + + public int floorY() { + return this.floorY; + } + + public int ceilingY() { + return this.ceilingY; + } + } + + public enum RoomCorner { + NORTH_WEST, + NORTH_EAST, + SOUTH_WEST, + SOUTH_EAST + } +} diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json index a4603b2..1fcb59f 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -522,7 +522,7 @@ "message.sanctuary.backroom.registry_busy": "The Backroom registry is settling too many interrupted transfers. Try again later.", "message.sanctuary.backroom.mailbox_missing": "Load and register your Mailbox before entering the Backroom.", "message.sanctuary.backroom.mailbox_full": "Your Mailbox has no room for all your gear, companion egg and cape.", - "message.sanctuary.backroom.entered": "You slip through the bed with no equipment…", + "message.sanctuary.backroom.entered": "You slip through the bed with no equipment… A torch marks your arrival point.", "message.sanctuary.backroom.bed_entry_only": "Only a bed can take you into the Backroom.", "message.sanctuary.backroom.exited": "The bed returns you to your spawn point with your findings.", "message.sanctuary.backroom.exited_with_losses": "The bed returns you with your findings and settles the recovered lost items.", diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json index 3bcbc65..787c366 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -522,7 +522,7 @@ "message.sanctuary.backroom.registry_busy": "Le registre Backroom termine trop de transferts interrompus. Réessaie plus tard.", "message.sanctuary.backroom.mailbox_missing": "Charge et enregistre ta Mailbox avant d’entrer dans la Backroom.", "message.sanctuary.backroom.mailbox_full": "Ta Mailbox n’a pas assez de place pour tout ton équipement, l’œuf et la cape.", - "message.sanctuary.backroom.entered": "Tu traverses le lit sans aucun équipement…", + "message.sanctuary.backroom.entered": "Tu traverses le lit sans aucun équipement… Une torche marque ton point d’arrivée.", "message.sanctuary.backroom.bed_entry_only": "Seul un lit permet d’entrer dans la Backroom.", "message.sanctuary.backroom.exited": "Le lit te ramène à ton point d’apparition avec tes trouvailles.", "message.sanctuary.backroom.exited_with_losses": "Le lit te ramène avec tes trouvailles et valide les objets perdus récupérés.", diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json b/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json index e58b506..a089e80 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json @@ -478,7 +478,7 @@ "message.sanctuary.backroom.registry_busy": "Реестр Бэкрума завершает слишком много прерванных переносов. Повторите попытку позже.", "message.sanctuary.backroom.mailbox_missing": "Загрузите и зарегистрируйте почтовый ящик перед входом в Бэкрум.", "message.sanctuary.backroom.mailbox_full": "В почтовом ящике недостаточно места для снаряжения, яйца спутника и плаща.", - "message.sanctuary.backroom.entered": "Вы проваливаетесь сквозь кровать без снаряжения…", + "message.sanctuary.backroom.entered": "Вы проваливаетесь сквозь кровать без снаряжения… Факел отмечает место прибытия.", "message.sanctuary.backroom.bed_entry_only": "В Бэкрум можно попасть только через кровать.", "message.sanctuary.backroom.exited": "Кровать возвращает вас к точке возрождения со всеми находками.", "message.sanctuary.backroom.exited_with_losses": "Кровать возвращает вас с находками и закрепляет найденные потерянные предметы.", diff --git a/sanctuary/src/main/resources/data/sanctuary/dimension_type/backroom.json b/sanctuary/src/main/resources/data/sanctuary/dimension_type/backroom.json index cd91977..b0ad1f6 100644 --- a/sanctuary/src/main/resources/data/sanctuary/dimension_type/backroom.json +++ b/sanctuary/src/main/resources/data/sanctuary/dimension_type/backroom.json @@ -1,5 +1,5 @@ { - "ambient_light": 0.35, + "ambient_light": 0.0, "attributes": { "minecraft:audio/ambient_sounds": { "mood": { @@ -17,25 +17,25 @@ "minecraft:gameplay/respawn_anchor_works": false, "minecraft:gameplay/sky_light_level": 0.0, "minecraft:gameplay/water_evaporates": false, - "minecraft:visual/ambient_light_color": "#d5d8ce", - "minecraft:visual/fog_color": "#b7b9ad", - "minecraft:visual/fog_end_distance": 112.0, - "minecraft:visual/fog_start_distance": 20.0, - "minecraft:visual/sky_light_color": "#d9dccf", + "minecraft:visual/ambient_light_color": "#000000", + "minecraft:visual/fog_color": "#000000", + "minecraft:visual/fog_end_distance": 96.0, + "minecraft:visual/fog_start_distance": 12.0, + "minecraft:visual/sky_light_color": "#000000", "minecraft:visual/sky_light_factor": 0.0 }, "cardinal_light": "nether", "coordinate_scale": 1.0, - "has_ceiling": false, + "has_ceiling": true, "has_ender_dragon_fight": false, "has_fixed_time": true, "has_skylight": false, - "height": 192, + "height": 448, "infiniburn": "#minecraft:infiniburn_overworld", - "logical_height": 192, - "min_y": 0, - "monster_spawn_block_light_limit": 15, - "monster_spawn_light_level": 7, + "logical_height": 448, + "min_y": -64, + "monster_spawn_block_light_limit": 0, + "monster_spawn_light_level": 0, "skybox": "none", "timelines": "#minecraft:in_nether" } diff --git a/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/backroom.json b/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/backroom.json index 7dd8d55..3ca7544 100644 --- a/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/backroom.json +++ b/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/backroom.json @@ -6,22 +6,18 @@ "effects": { "water_color": "#596f75", "water_fog_color": "#303b3e", - "fog_color": "#b7b9ad", - "sky_color": "#b7b9ad", + "fog_color": "#000000", + "sky_color": "#000000", "grass_color": "#70845d", "foliage_color": "#657b58" }, - "creature_spawn_probability": 0.08, + "creature_spawn_probability": 0.0, "spawners": { "ambient": [], "axolotls": [], "creature": [], "misc": [], - "monster": [ - { "type": "minecraft:zombie", "maxCount": 2, "minCount": 1, "weight": 7 }, - { "type": "minecraft:skeleton", "maxCount": 2, "minCount": 1, "weight": 7 }, - { "type": "minecraft:enderman", "maxCount": 1, "minCount": 1, "weight": 2 } - ], + "monster": [], "underground_water_creature": [], "water_ambient": [], "water_creature": [] diff --git a/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/backroom.json b/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/backroom.json index b02a172..ab96ba7 100644 --- a/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/backroom.json +++ b/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/backroom.json @@ -2,11 +2,11 @@ "aquifers_enabled": false, "default_block": { "Name": "minecraft:air" }, "default_fluid": { "Name": "minecraft:air" }, - "disable_mob_generation": false, + "disable_mob_generation": true, "legacy_random_source": false, "noise": { - "height": 192, - "min_y": 0, + "height": 448, + "min_y": -64, "size_horizontal": 1, "size_vertical": 2 }, diff --git a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomModelSmoke.java b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomModelSmoke.java index e385155..395d704 100644 --- a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomModelSmoke.java +++ b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomModelSmoke.java @@ -1,8 +1,14 @@ package fr.koka99cab.sanctuary26.sanctuary.backroom; import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature; -import java.util.EnumSet; +import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout; +import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.BackroomLevel; +import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.RoomCorner; +import java.util.EnumMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; import net.minecraft.SharedConstants; import net.minecraft.server.Bootstrap; @@ -18,7 +24,9 @@ public final class BackroomModelSmoke { entryTransactionsAreBounded(); personalRecoveryIsPrioritized(); recentLossesRemainPersonal(); - architectureIsDeterministicAndComposed(); + architectureIsDeterministicAndStacked(); + roomCornersReachEveryVariant(); + mazeDoorsStayConnected(); } private static void entryTransactionsAreBounded() { @@ -74,27 +82,66 @@ public final class BackroomModelSmoke { "An ownerless loss was not immediately eligible for shared loot"); } - private static void architectureIsDeterministicAndComposed() { + private static void architectureIsDeterministicAndStacked() { long seed = 26_200_132L; - EnumSet families = - EnumSet.noneOf(BackroomArchitectureFeature.Architecture.class); - for (int macroX = -12; macroX <= 12; macroX++) for (int macroZ = -12; macroZ <= 12; macroZ++) { - int chunkX = macroX * 8, chunkZ = macroZ * 8; - BackroomArchitectureFeature.Architecture family = - BackroomArchitectureFeature.architecture(seed, chunkX, chunkZ); - families.add(family); - require(family == BackroomArchitectureFeature.architecture(seed, chunkX + 7, chunkZ + 7), - "One macro cell mixed unrelated architecture palettes"); - require(BackroomArchitectureFeature.fingerprint(seed, chunkX, chunkZ) - == BackroomArchitectureFeature.fingerprint(seed, chunkX, chunkZ), - "Architecture fingerprint is not deterministic"); + require(BackroomLayout.WORLD_MIN_Y == -64 && BackroomLayout.WORLD_MAX_Y_EXCLUSIVE == 384, + "Backroom no longer occupies the complete requested vertical range"); + for (int y = BackroomLayout.WORLD_MIN_Y; y < BackroomLayout.WORLD_MAX_Y_EXCLUSIVE; y++) { + require(BackroomLayout.levelAt(y) != null, "A Y layer is not assigned to one of the five levels"); } - require(families.size() == BackroomArchitectureFeature.Architecture.values().length, - "Not all authored architecture families are reachable"); + require(BackroomLevel.values().length == 5, "Backroom no longer has exactly five stacked levels"); + require(BackroomLayout.levelAt(BackroomLayout.ENTRY_FLOOR_Y) == BackroomLevel.LEVEL_0, + "Arrival deck left the top level"); + for (int transition = 0; transition < 4; transition++) { + boolean found = false; + for (int x = -64; x < 64 && !found; x++) for (int z = -64; z < 64; z++) { + if (BackroomLayout.transitionHole(seed, transition, x, z)) { + found = true; + break; + } + } + require(found, "One level transition has no reachable authored opening"); + } + require(BackroomArchitectureFeature.fingerprint(seed, 12, -7) + == BackroomArchitectureFeature.fingerprint(seed, 12, -7), + "Architecture fingerprint is not deterministic"); + require(BackroomLayout.cityHighway(8, 1_423) && !BackroomLayout.cityHighway(48, 48), + "City highways are no longer long continuous axes separated from building cells"); require(BackroomArchitectureFeature.withinPlayableArea(2_000, 0), "Playable boundary moved inward"); require(!BackroomArchitectureFeature.withinPlayableArea(2_001, 0), "Architecture escaped the 2,000-block zone"); } + private static void roomCornersReachEveryVariant() { + long seed = 26_200_132L; + Map> variants = new EnumMap<>(RoomCorner.class); + for (RoomCorner corner : RoomCorner.values()) variants.put(corner, new HashSet<>()); + for (int x = -32; x <= 32; x++) for (int z = -32; z <= 32; z++) { + for (RoomCorner corner : RoomCorner.values()) { + variants.get(corner).add(BackroomLayout.cornerVariant(seed, BackroomLevel.LEVEL_0, x, z, corner)); + } + } + for (RoomCorner corner : RoomCorner.values()) { + require(variants.get(corner).equals(Set.of(0, 1, 2, 3, 4)), + "A room corner lost one of its five authored material variants: " + corner); + } + } + + private static void mazeDoorsStayConnected() { + long seed = 26_200_132L; + for (int x = -16; x <= 16; x++) for (int z = -16; z <= 16; z++) { + if (x == 0 && z == 0) continue; + boolean descends = false; + for (int[] direction : new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) { + int neighborX = x + direction[0], neighborZ = z + direction[1]; + boolean forward = BackroomLayout.mazeConnected(seed, x, z, neighborX, neighborZ); + boolean reverse = BackroomLayout.mazeConnected(seed, neighborX, neighborZ, x, z); + require(forward == reverse, "Maze doorway is not symmetric across a room boundary"); + if (forward && Math.abs(neighborX) + Math.abs(neighborZ) < Math.abs(x) + Math.abs(z)) descends = true; + } + require(descends, "A generated maze room has no open route toward the connected root"); + } + } + private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); } -- 2.54.0