Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39c271c96 |
@@ -6,6 +6,18 @@ 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é
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-alpha.57`**.
|
||||
|
||||
- ajoute la dimension persistante `sanctuary:backroom`, accessible en s’accroupissant devant un lit et quittée uniquement par un lit, avec arrivées variées dans une zone architecturale de 2 000 blocs ;
|
||||
- transfère atomiquement vers la Mailbox tout l’équipement d’entrée, y compris les rangées étendues, l’œuf compagnon et la cape, avec reprise idempotente après interruption ;
|
||||
- conserve les objets perdus au dépop, dans le vide et lors des morts en Backroom dans un registre borné et non duplicable ;
|
||||
- transforme tous les coffres de la dimension en butin renouvelable, pauvre la plupart du temps, qui privilégie les pertes personnelles dans les premiers coffres avant les pertes anciennes partagées ;
|
||||
- génère des villes, bureaux, plaines, agrégats flottants et pièces visuellement retournées à partir de palettes composées, en réservant les matériaux d’Another World aux anomalies et les blocs précieux à de rares cachettes ;
|
||||
- fait enregistrer la mousse turquoise par Another World via l’API publique de matériaux Backroom, sans dépendance circulaire ;
|
||||
- migre Sanctuary vers les données v14 sans changer le protocole réseau 24, sans réinitialiser les sauvegardes ni effacer les constructions.
|
||||
|
||||
## `26.2.0-alpha.226`
|
||||
|
||||
Module modifié : **Sanctuary `0.0.0-alpha.130`**. Les neuf autres modules conservent leur version issue de l'alpha.225.
|
||||
|
||||
@@ -1956,7 +1956,7 @@ tasks.register("verifyAnotherWorldCatalog") {
|
||||
"src/main/resources/assets/redstoner/textures/item/weather_gauge.png",
|
||||
"src/main/resources/data/redstoner/recipe/weather_gauge.json"
|
||||
]
|
||||
if (project.version.toString() != "0.0.0-alpha.56"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44"
|
||||
|| sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2
|
||||
|| !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")')
|
||||
@@ -3273,7 +3273,7 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
"pack/migrations/26.2.0-alpha.188-beta-climate-provinces-to-alpha.189.json"))
|
||||
def mixer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.56"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.188"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.189"
|
||||
|| migration.target?.modules?.anotherworld != "0.0.0-alpha.48"
|
||||
@@ -3300,3 +3300,33 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyClimateBiomeMixer")
|
||||
}
|
||||
|
||||
tasks.register("verifyBackroomMaterialIntegration") {
|
||||
group = "verification"
|
||||
description = "Checks the one-way Another World material integration with Sanctuary Backroom."
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java"))
|
||||
inputs.file(rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"))
|
||||
doLast {
|
||||
def initializer = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java").text
|
||||
def blocks = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java").text
|
||||
def api = rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
|
||||
def rootBuild = rootProject.file("build.gradle").text
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| !initializer.contains("BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS)")
|
||||
|| !blocks.contains('"turquoise_moss"')
|
||||
|| !api.contains("public static void registerAnomalyMaterial")
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.131"
|
||||
|| !rootBuild.contains('project(":anotherworld")')
|
||||
|| !rootBuild.contains('implementation project(":sanctuary")')) {
|
||||
throw new GradleException("Another World alpha.57 Backroom material integration is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyBackroomMaterialIntegration")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.item.WeatherCompassItem;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.network.AnotherWorldNetworking;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldRegistries;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlocks;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -26,6 +28,7 @@ public final class AnotherWorldMod implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
AnotherWorldRegistries.initialize();
|
||||
BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS);
|
||||
VaultService.initialize();
|
||||
WeatherCompassItem.registerFrameInteraction();
|
||||
CanonMod.initialize();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"ambiance": "*",
|
||||
"sanctuary": ">=0.0.0-alpha.117"
|
||||
"sanctuary": ">=0.0.0-alpha.131"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
+51
-44
@@ -2916,11 +2916,11 @@ 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.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| rootProject.masterkey_version != "0.0.0-alpha.9"
|
||||
|| rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.ouch.depends?.itsalive != ">=0.0.0-alpha.37"
|
||||
|| !registry.contains('Identifier.fromNamespaceAndPath("portals", "notch")')
|
||||
|| !resident.contains("setCanonicalUuid(canonical.getUUID())")
|
||||
@@ -3010,7 +3010,7 @@ tasks.register("verifyMagicCarpet") {
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.iliketomoveit.depends?.sanctuary != ">=0.0.0-alpha.127"
|
||||
@@ -3018,7 +3018,7 @@ tasks.register("verifyMagicCarpet") {
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.onlyfun.depends?.iliketomoveit != ">=0.0.0-alpha.17"
|
||||
|| manifests.onlyfun.depends?.sanctuary != ">=0.0.0-alpha.129"
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.223"
|
||||
@@ -3641,7 +3641,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
|
||||
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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"]
|
||||
@@ -3691,7 +3691,7 @@ tasks.register("verifyMirrorLavenderDrawerRelease") {
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java").text
|
||||
def drawerRecipe = file("redstoner/src/main/resources/data/redstoner/recipe/drawer.json")
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.191"
|
||||
@@ -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.226"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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.226"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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.226"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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.226"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -4903,7 +4903,7 @@ tasks.register("verifyQuestRequestEscrowRelease") {
|
||||
|| migration.quest_request_escrow?.kill_progress_automatic != true
|
||||
|| migration.quest_request_escrow?.reward_delivery_to_claimant_mailbox != true
|
||||
|| migration.compatibility?.sanctuary_target_network_protocol != 21
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 13
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 14
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| release.pack_version != "26.2.0-alpha.226"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.226.mrpack"
|
||||
@@ -4945,10 +4945,10 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file(
|
||||
"sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| 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.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.source?.modules != [
|
||||
itsalive: "0.0.0-alpha.30",
|
||||
@@ -4982,7 +4982,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 4
|
||||
|| moveItManifest.custom?.sanctuary26?.data_version != 4
|
||||
|| moveItManifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 13
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 14
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| release.pack_version != "26.2.0-alpha.226"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.226.mrpack"
|
||||
@@ -5105,8 +5105,8 @@ tasks.register("verifySafeVaultFeature") {
|
||||
image == null || image.width != size[0] || image.height != size[1]
|
||||
}.keySet()
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.schema_version != 1
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.202"
|
||||
@@ -5226,7 +5226,7 @@ tasks.register("verifySafeVaultFeature") {
|
||||
|| unlimitedMigration.compatibility?.registry_ids_changed != false
|
||||
|| unlimitedMigration.compatibility?.pack_release_updated != true
|
||||
|| unlimitedMigration.distribution?.packwiz_index_refreshed != true
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.117"
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.131"
|
||||
|| manifest.custom?.sanctuary26?.data_version != 5
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 2
|
||||
|| !file("build.gradle").text.contains('implementation project(":sanctuary")')
|
||||
@@ -5297,7 +5297,7 @@ tasks.register("verifySafeVaultInterfaceAlpha203Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.202"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.50"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.203"
|
||||
@@ -5343,7 +5343,7 @@ tasks.register("verifySafeVaultFixAlpha204Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.203"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.51"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.204"
|
||||
@@ -5397,7 +5397,7 @@ tasks.register("verifySafeVaultCubeAlpha205Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.204"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.52"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.205"
|
||||
@@ -5455,7 +5455,7 @@ tasks.register("verifySafeVaultCtmAlpha206Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.205"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.53"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.206"
|
||||
@@ -5507,7 +5507,7 @@ tasks.register("verifySafeVaultUnlimitedAlpha207Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.206"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.54"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.207"
|
||||
@@ -5966,7 +5966,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
@@ -6003,7 +6003,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
|| migration.distribution?.jei?.target != "30.28.0.193"
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "qaCQ7LGO"
|
||||
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
@@ -6068,9 +6068,9 @@ tasks.register("verifyAlpha211Release") {
|
||||
itsalive: "0.0.0-alpha.38",
|
||||
ouch: "0.0.0-alpha.11",
|
||||
masterkey: "0.0.0-alpha.9"]
|
||||
def workspaceModules = currentModules
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
|
||||
def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11]
|
||||
def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 13]
|
||||
def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 14]
|
||||
def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22]
|
||||
def workspaceProtocols = expectedProtocols + [sanctuary: 24]
|
||||
def manifests = expectedTarget.collectEntries { id, version ->
|
||||
@@ -6168,7 +6168,7 @@ tasks.register("verifyAlpha212Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -6186,7 +6186,7 @@ tasks.register("verifyAlpha212Release") {
|
||||
|| migration.distribution?.packwiz_index_refreshed != true
|
||||
|| migration.distribution?.prism_modrinth_index_updated != true
|
||||
|| migration.distribution?.shared_client_server_mrpack != true
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !minecraftMixin.contains('method = "pickBlockOrEntity"')
|
||||
|| release.pack_version != "26.2.0-alpha.226"
|
||||
@@ -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.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.217"
|
||||
|| migration.source?.modules != sourceModules
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.218"
|
||||
@@ -6691,7 +6691,7 @@ tasks.register("verifyAlpha218Release") {
|
||||
|| manifests.redstoner.custom?.sanctuary26?.network_protocol != 7
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !rules.contains("MAX_CONTAINERS = 128")
|
||||
|| !rules.contains("PAGE_SIZE = 54")
|
||||
@@ -6753,7 +6753,7 @@ tasks.register("verifyAlpha219Release") {
|
||||
ambiance: "0.0.0-alpha.22", sanctuary: "0.0.0-alpha.130",
|
||||
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
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
|
||||
def requiredJars = currentModules.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
@@ -6793,7 +6793,7 @@ tasks.register("verifyAlpha219Release") {
|
||||
|| migration.compatibility?.iliketomoveit_network_protocol != 5
|
||||
|| migration.compatibility?.ambiance_data_version != 2
|
||||
|| migration.compatibility?.ambiance_network_protocol != 6
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
@@ -7078,7 +7078,7 @@ tasks.register("verifyAlpha222Release") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.221"
|
||||
|| migration.source?.modules != [
|
||||
redstoner: "0.0.0-alpha.12", sanctuary: "0.0.0-alpha.122"]
|
||||
@@ -7120,7 +7120,7 @@ tasks.register("verifyAlpha222Release") {
|
||||
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|
||||
|| manifests.redstoner.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.redstoner.custom?.sanctuary26?.network_protocol != 7
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !rules.contains("MAX_SEARCH_LENGTH = 32")
|
||||
|| !rules.contains("compactCount(int count)")
|
||||
@@ -7236,7 +7236,7 @@ tasks.register("verifyAlpha223Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -7272,7 +7272,7 @@ tasks.register("verifyAlpha223Release") {
|
||||
|| migration.distribution?.jei?.target != "30.29.0.198"
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "67Dvn8sL"
|
||||
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !inventorySort.contains("COOLDOWN_TICKS = 8")
|
||||
|| !inventorySort.contains("if (row == 0) continue")
|
||||
@@ -7357,6 +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.130"]
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
|
||||
def inventorySort = file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryInventorySort.java").text
|
||||
def clientProgress = file(
|
||||
@@ -7376,7 +7377,7 @@ tasks.register("verifyAlpha224Release") {
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| currentModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| workspaceModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.223"
|
||||
|| migration.source?.modules != sourceModules
|
||||
@@ -7407,7 +7408,7 @@ tasks.register("verifyAlpha224Release") {
|
||||
|| migration.distribution?.prism_modrinth_index_updated != true
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !inventorySort.contains("instanceof ChestMenu")
|
||||
|| !inventorySort.contains("instanceof ShulkerBoxMenu")
|
||||
@@ -7477,10 +7478,14 @@ tasks.register("verifyAlpha225Release") {
|
||||
iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27",
|
||||
sanctuary: "0.0.0-alpha.129"]
|
||||
def currentModules = targetModules + [sanctuary: "0.0.0-alpha.130"]
|
||||
def currentWorkspaceModules = currentModules + [
|
||||
anotherworld: "0.0.0-alpha.57", sanctuary: "0.0.0-alpha.131"]
|
||||
def workspaceModules = moduleIds.collectEntries { id ->
|
||||
[(id): project(":${id}").version.toString()]
|
||||
}
|
||||
def requiredJars = workspaceModules.collect { id, version ->
|
||||
def releasedModules = workspaceModules + [
|
||||
anotherworld: "0.0.0-alpha.56", sanctuary: "0.0.0-alpha.130"]
|
||||
def requiredJars = releasedModules.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
def obsoleteJars = sourceModules.collect { id, version ->
|
||||
@@ -7518,7 +7523,7 @@ tasks.register("verifyAlpha225Release") {
|
||||
def carpetTextureHash = MessageDigest.getInstance("SHA-256")
|
||||
.digest(carpetTexture.bytes).encodeHex().toString()
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| currentModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| currentWorkspaceModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.224"
|
||||
|| migration.source?.modules != sourceModules
|
||||
@@ -7639,11 +7644,13 @@ tasks.register("verifyAlpha226Release") {
|
||||
def workspaceModules = moduleIds.collectEntries { id ->
|
||||
[(id): project(":${id}").version.toString()]
|
||||
}
|
||||
def requiredJars = workspaceModules.collect { id, version ->
|
||||
def releasedModules = workspaceModules + [
|
||||
anotherworld: "0.0.0-alpha.56", sanctuary: "0.0.0-alpha.130"]
|
||||
def requiredJars = releasedModules.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.225"
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.129"]
|
||||
@@ -7660,7 +7667,7 @@ tasks.register("verifyAlpha226Release") {
|
||||
|| migration.compatibility?.existing_saves_supported != true
|
||||
|| migration.compatibility?.new_world_required != false
|
||||
|| migration.compatibility?.save_reset_required != false
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| baseRecipe.type != "minecraft:crafting_shaped"
|
||||
|| baseRecipe.pattern != ["WW", "WW", "WW"]
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
# Sanctuary TikTok — organisation de réalisation
|
||||
|
||||
Ce document transforme les [100 premiers scripts](VIDEOS_001_100.md) en chaîne de production solo, reproductible et extensible. L'objectif est de produire vite sans perdre la sobriété, la précision technique ni la cohérence de Sanctuary.
|
||||
|
||||
## Objectif de la saison 001
|
||||
|
||||
- Produire les 100 vidéos en **10 à 12 semaines**.
|
||||
- Publier **5 vidéos par semaine**, soit 20 semaines de diffusion.
|
||||
- Maintenir en permanence **au moins 15 vidéos terminées d'avance**.
|
||||
- Valider le langage visuel sur 12 pilotes avant d'industrialiser les 88 suivantes.
|
||||
- Utiliser les performances pour choisir les prochains sujets, sans réécrire l'identité de la série à chaque publication.
|
||||
|
||||
Charge solo réaliste : **14 à 18 heures par semaine**, soit environ **140 à 180 heures** pour la saison complète. Les vidéos d'interface peuvent demander moins d'une heure ; les scènes multijoueur, worldgen ou avant/après authentiques peuvent en demander deux à quatre.
|
||||
|
||||
## Principe directeur
|
||||
|
||||
La publication suit les numéros. La réalisation suit les décors.
|
||||
|
||||
Tourner dix vidéos partageant la même ferme, le même laboratoire ou la même lumière est plus efficace que de réaliser les épisodes 001 à 010 dans l'ordre. Le numéro reste l'identité publique de la vidéo ; le lot reste son identité de production.
|
||||
|
||||
## Les neuf plateaux de tournage
|
||||
|
||||
| Code | Plateau | Épisodes principaux | Préparation indispensable |
|
||||
|---|---|---|---|
|
||||
| `UI` | Laboratoire interface | 003–019, 083–086, 097 | profils de progression, inventaires remplis, terminal et Shop |
|
||||
| `WRLD` | Archipel et worldgen | 002, 020–027, 053, 072–079 | seed verrouillée, points de vue, météo et heures reproductibles |
|
||||
| `ALPH` | Alpha / maison Indev | 028–031 | portail, maison intacte, état de Notch et récompense réinitialisable |
|
||||
| `MOVE` | Terrain de déplacement | 032–039 | tours, deux niveaux de Waystones, ligne de poulies et trajectoire de grappin |
|
||||
| `PASS` | Bestiaire calme | 040–049, 053 | enclos invisibles ou hors champ, variantes préparées, fleurs et reproduction |
|
||||
| `HOST` | Bestiaire hostile | 050–059 | arène sombre, protections opérateur, cibles mobiles et second joueur si nécessaire |
|
||||
| `FARM` | Ferme et cuisine | 060–071 | parcelles par stade, six postes, pupitre, stocks d'ingrédients et deux joueurs |
|
||||
| `TECH` | Atelier technologique | 080–090 | Canon, terminal, ordinateurs, réseau de coffres et deux coffres-forts appartenant à des joueurs distincts |
|
||||
| `OUCH` | Stand de tir / place serveur | 091–100 | cibles, munitions, protections, Shop, panneaux, Mailboxes et second joueur |
|
||||
|
||||
Les commandes MasterKey servent à préparer les acteurs et les états, jamais à apparaître dans l'image finale. Les scènes doivent ensuite montrer le comportement de jeu normal.
|
||||
|
||||
## Les six vagues de production
|
||||
|
||||
### P — Pilote, 12 vidéos
|
||||
|
||||
`001, 003, 006, 014, 020, 031, 041, 053, 062, 077, 086, 099`
|
||||
|
||||
But : éprouver le manifeste, l'interface, le monde, une mécanique spectaculaire, une créature, l'agriculture, l'ambiance et le multijoueur.
|
||||
|
||||
Livrable : 12 exports finalisés, mais seulement les quatre premiers sont programmés immédiatement. Les huit autres restent disponibles pour corriger le système avant une diffusion complète.
|
||||
|
||||
### A — Progression et interfaces, 20 vidéos
|
||||
|
||||
`002, 004, 005, 007–013, 015–019, 082–085, 097`
|
||||
|
||||
But : rentabiliser le laboratoire UI et verrouiller la manière de filmer les menus verticaux.
|
||||
|
||||
### B — Monde et Ambiance, 19 vidéos
|
||||
|
||||
`021–030, 072–076, 078–081`
|
||||
|
||||
But : tourner toutes les variations de jour, nuit, météo, shader et dimension pendant que la seed et les cadrages sont verrouillés.
|
||||
|
||||
### C — Déplacement et bestiaire, 20 vidéos
|
||||
|
||||
`032–040, 042–052`
|
||||
|
||||
But : mutualiser les acteurs, les enclos, les caméras latérales et les scènes nécessitant du mouvement.
|
||||
|
||||
### D — Infectés, agriculture et Canaplia, 17 vidéos
|
||||
|
||||
`054–061, 063–071`
|
||||
|
||||
But : partager l'arène hostile, la ferme à stades contrôlés, les ingrédients et le deuxième joueur.
|
||||
|
||||
### E — Technologie, combat et systèmes sociaux, 12 vidéos
|
||||
|
||||
`087–096, 098, 100`
|
||||
|
||||
But : terminer avec les scènes demandant le plus de préparation matérielle et de règles serveur.
|
||||
|
||||
Ces six listes couvrent les 100 épisodes une seule fois. Une vidéo pilote ne doit pas être retournée pendant sa vague thématique sauf si le test révèle un défaut concret.
|
||||
|
||||
## Calendrier conseillé
|
||||
|
||||
### Semaine 0 — Installer l'usine
|
||||
|
||||
- Verrouiller la seed, la version du pack et les réglages vidéo.
|
||||
- Créer les neuf plateaux ou sauvegardes de référence.
|
||||
- Choisir la typographie, la taille du texte et l'unique animation de carton final.
|
||||
- Créer un projet de montage modèle avec trois pistes vidéo, voix, sons du jeu, ambiance et sous-titres.
|
||||
- Exporter un test de dix secondes et le regarder sur un téléphone réel.
|
||||
- Préparer les douze fiches du pilote et leurs commandes de laboratoire.
|
||||
|
||||
### Semaine 1 — Produire le pilote
|
||||
|
||||
- Jour 1 : préparer les scènes `003, 006, 014, 086`.
|
||||
- Jour 2 : capturer `001, 020, 053, 077` dans le monde.
|
||||
- Jour 3 : capturer `031, 041, 062, 099` et les scènes multijoueur.
|
||||
- Jour 4 : monter les douze versions brutes.
|
||||
- Jour 5 : voix, texte, son, exports et contrôle qualité.
|
||||
- Jour 6 : corrections et programmation des quatre premières publications.
|
||||
|
||||
### Semaine 2 — Publier et corriger le système
|
||||
|
||||
- Publier quatre vidéos espacées.
|
||||
- Relever les performances à J+1 sans changer les vidéos déjà programmées au premier commentaire négatif.
|
||||
- Identifier les problèmes répétés de lisibilité, de rythme ou de compréhension.
|
||||
- Corriger uniquement le modèle de production concerné : accroche, taille de texte, cadrage, son ou durée.
|
||||
- Commencer la vague A pendant la diffusion.
|
||||
|
||||
### Semaines 3 à 11 — Régime de croisière
|
||||
|
||||
- Produire environ dix nouvelles vidéos par semaine.
|
||||
- Publier cinq vidéos par semaine.
|
||||
- Consacrer une semaine complète aux grandes vagues A, B et C lorsqu'elles exigent davantage de scènes.
|
||||
- Garder la semaine 11 pour les reprises, les scènes multijoueur manquantes et les exports finaux.
|
||||
|
||||
### Semaines 12 à 20 — Diffuser avec une saison d'avance
|
||||
|
||||
- Continuer la publication des 100 vidéos.
|
||||
- Concevoir la saison 101–200 à partir des formats qui fonctionnent, pas seulement des sujets qui ont fait le plus de vues.
|
||||
- Maintenir un tampon minimum de quinze exports prêts.
|
||||
|
||||
## Semaine type en régime de croisière
|
||||
|
||||
| Jour | Travail | Durée cible | Livrable |
|
||||
|---|---|---:|---|
|
||||
| Lundi | verrouillage de dix scripts, vérification des sources, liste des accessoires | 2 h | dix fiches `LOCKED` |
|
||||
| Mardi | préparation des scènes, acteurs, inventaires, heure et météo | 3 h | dix scènes `READY` |
|
||||
| Mercredi | capture groupée du lot | 4 h | rushes nommés et vérifiés |
|
||||
| Jeudi | montage image des dix vidéos | 4 h | dix montages `V1` |
|
||||
| Vendredi | voix, texte, mixage, export et QC | 3 à 4 h | dix masters `APPROVED` |
|
||||
| Samedi | programmation et revue des publications précédentes | 1 h | calendrier et décisions J+7 |
|
||||
|
||||
Ne pas imposer cette répartition lorsque la scène dicte autre chose. Une nuit de sang, un combat multijoueur ou une météo rare doivent être capturés en série pendant que leur état est disponible.
|
||||
|
||||
## Pipeline d'une vidéo
|
||||
|
||||
Chaque vidéo passe par un seul état à la fois :
|
||||
|
||||
```text
|
||||
BACKLOG
|
||||
→ LOCKED
|
||||
→ READY
|
||||
→ CAPTURED
|
||||
→ V1
|
||||
→ TEXT_AUDIO
|
||||
→ QC
|
||||
→ APPROVED
|
||||
→ SCHEDULED
|
||||
→ PUBLISHED
|
||||
→ REVIEWED
|
||||
```
|
||||
|
||||
- `BACKLOG` : sujet présent dans le catalogue.
|
||||
- `LOCKED` : accroche, plans, texte et source validés pour la version distribuée du pack.
|
||||
- `READY` : décor, acteurs, objets et état du monde préparés.
|
||||
- `CAPTURED` : tous les rushes existent, sont lisibles et sauvegardés.
|
||||
- `V1` : montage image complet sans finition.
|
||||
- `TEXT_AUDIO` : texte exact, voix et sons finalisés.
|
||||
- `QC` : contrôle sur ordinateur puis téléphone.
|
||||
- `APPROVED` : master immuable prêt à publier.
|
||||
- `SCHEDULED` : date, légende et couverture enregistrées.
|
||||
- `PUBLISHED` : URL et date réelles ajoutées au suivi.
|
||||
- `REVIEWED` : performances J+1 et J+7 enregistrées, décision de suite prise.
|
||||
|
||||
Une vidéo qui échoue au contrôle revient à l'état concerné. Elle ne saute jamais directement de `CAPTURED` à `SCHEDULED`.
|
||||
|
||||
## Arborescence locale des médias
|
||||
|
||||
Les rushes et exports sont trop lourds pour Git. Les conserver hors du dépôt, par exemple sous `/Users/koka/Movies/Sanctuary-TikTok/Season-001/` :
|
||||
|
||||
```text
|
||||
Season-001/
|
||||
├── 00-shared/
|
||||
│ ├── brand/
|
||||
│ ├── music-and-sfx/
|
||||
│ ├── project-template/
|
||||
│ └── export-presets/
|
||||
├── 01-pilot/
|
||||
├── 02-wave-a-ui/
|
||||
├── 03-wave-b-world/
|
||||
├── 04-wave-c-movement-living/
|
||||
├── 05-wave-d-food-canaplia/
|
||||
├── 06-wave-e-tech-combat-social/
|
||||
└── episodes/
|
||||
└── SNC-TT-001/
|
||||
├── brief/
|
||||
├── capture/
|
||||
├── audio/
|
||||
├── project/
|
||||
├── exports/
|
||||
└── cover/
|
||||
```
|
||||
|
||||
Le dépôt Git conserve uniquement les scripts, le plan, les éventuels manifests texte et les décisions éditoriales. Il ne doit recevoir ni rushes, ni projets de montage lourds, ni exports vidéo.
|
||||
|
||||
## Convention de fichiers
|
||||
|
||||
Identifiant canonique : `SNC-TT-<numéro sur trois chiffres>`.
|
||||
|
||||
Exemples :
|
||||
|
||||
```text
|
||||
SNC-TT-041__capture-01__wide.mov
|
||||
SNC-TT-041__capture-02__flower.mov
|
||||
SNC-TT-041__voice-fr__v02.wav
|
||||
SNC-TT-041__master-fr__v03.mp4
|
||||
SNC-TT-041__cover-fr__v01.png
|
||||
SNC-TT-041__metadata.yml
|
||||
```
|
||||
|
||||
Règles :
|
||||
|
||||
- ne jamais employer `final-final`, `nouveau` ou `ok2` ;
|
||||
- incrémenter `v01`, `v02`, `v03` ;
|
||||
- ne jamais remplacer un master `APPROVED` ; créer une nouvelle version ;
|
||||
- conserver la langue dans le nom du master pour préparer EN et RU ;
|
||||
- inscrire la version exacte du pack capturé dans `metadata.yml` ;
|
||||
- conserver les musiques, sons et images avec leur provenance et leurs droits.
|
||||
|
||||
## Fiche de préparation obligatoire
|
||||
|
||||
Avant la capture, chaque épisode reçoit ces champs :
|
||||
|
||||
```yaml
|
||||
id: SNC-TT-041
|
||||
status: LOCKED
|
||||
pack_version: 26.2.0-alpha.226
|
||||
capture_batch: PASS
|
||||
format: interaction
|
||||
target_duration_seconds: 11
|
||||
source_verified: true
|
||||
actors: 1
|
||||
multiplayer_required: false
|
||||
world_snapshot: passive-mobs-v1
|
||||
shots:
|
||||
- dangerous-approach
|
||||
- flower-given
|
||||
- happy-follow
|
||||
rights_checked: true
|
||||
```
|
||||
|
||||
Le numéro de version sert à empêcher la publication tardive d'une règle devenue fausse après une mise à jour. Avant programmation, comparer le comportement capturé à la version actuellement distribuée.
|
||||
|
||||
## Préparation des scènes
|
||||
|
||||
### Décors
|
||||
|
||||
- Utiliser des volumes simples, de grandes surfaces lisibles et un fond sans détails concurrents.
|
||||
- Réserver la pièce blanche ou gris très clair aux vidéos de laboratoire.
|
||||
- Réserver le décor naturel aux découvertes et chroniques.
|
||||
- Conserver les coordonnées et l'orientation de chaque caméra dans la fiche de scène.
|
||||
- Pour un avant/après, réutiliser exactement la même caméra, la même heure et la même action.
|
||||
|
||||
### Caméras
|
||||
|
||||
N'utiliser que quatre familles de plans :
|
||||
|
||||
1. **preuve FPS** : interaction telle que le joueur la vit ;
|
||||
2. **caméra latérale fixe** : mouvement, créature ou mécanisme ;
|
||||
3. **plan large lent** : monde, météo ou révélation ;
|
||||
4. **gros plan d'interface** : valeur, état ou changement de rangée.
|
||||
|
||||
Un cinquième type de plan doit justifier son existence. Aucun mouvement de caméra ne doit concurrencer la mécanique montrée.
|
||||
|
||||
### Portrait
|
||||
|
||||
- Capturer directement en portrait lorsque l'interface est la feature.
|
||||
- Si la source est horizontale, cadrer toute l'action dans la zone verticale centrale dès le tournage.
|
||||
- Tester la taille du GUI et les sous-titres sur téléphone avant de tourner tout un lot.
|
||||
- Garder le texte loin des zones extrêmes supérieure et inférieure de l'écran.
|
||||
|
||||
### Multijoueur
|
||||
|
||||
Réserver une session acteur de deux à trois heures toutes les deux semaines. Y regrouper au minimum : `031, 037, 039, 048, 057, 065, 071, 090, 098 et 099`.
|
||||
|
||||
Préparer avant l'arrivée du second joueur : inventaires, permissions, Mailboxes, objectifs, position de caméra et état de sauvegarde. Le temps acteur doit servir à jouer la scène, pas à construire son décor.
|
||||
|
||||
## Montage minimaliste
|
||||
|
||||
### Structure par défaut
|
||||
|
||||
```text
|
||||
0,0–1,5 s preuve ou anomalie visible
|
||||
1,5–5,0 s action principale
|
||||
5,0–10,0 s conséquence
|
||||
10,0–14,0 s règle ou précision
|
||||
dernier 0,5–1,0 s carton Sanctuary
|
||||
```
|
||||
|
||||
Ce rythme est un point de départ. Une chronique contemplative peut respirer davantage ; une interaction évidente peut s'arrêter après huit secondes.
|
||||
|
||||
### Texte
|
||||
|
||||
- Une phrase d'accroche.
|
||||
- Une phrase de compréhension ou de conséquence.
|
||||
- Deux lignes maximum à la fois.
|
||||
- Aucun paragraphe explicatif.
|
||||
- Les nombres et noms techniques doivent être composés dans le logiciel de montage, jamais générés dans une image.
|
||||
- Les sous-titres reprennent la voix sans ajouter une deuxième narration concurrente.
|
||||
|
||||
### Son
|
||||
|
||||
- Conserver le son de l'action : clic, déplacement, impact, ouverture, souffle ou ambiance.
|
||||
- Utiliser le silence comme contraste avant une révélation.
|
||||
- Éviter une musique différente à chaque vidéo ; une petite famille sonore suffit à créer la série.
|
||||
- Vérifier les droits de tout son extérieur au jeu.
|
||||
- Pour le disque blanc, n'utiliser qu'un extrait dont la publication est autorisée.
|
||||
|
||||
## Contrôle qualité avant publication
|
||||
|
||||
Une vidéo est `APPROVED` seulement si toutes les réponses sont oui :
|
||||
|
||||
- La feature est-elle compréhensible sans lire la légende TikTok ?
|
||||
- La première preuve apparaît-elle dans les deux premières secondes ?
|
||||
- Une seule idée principale est-elle défendue ?
|
||||
- Les valeurs et comportements correspondent-ils encore à la version distribuée ?
|
||||
- Le HUD est-il présent uniquement lorsqu'il sert de preuve ?
|
||||
- Les textes restent-ils lisibles sur téléphone et limités à deux lignes ?
|
||||
- L'action reste-t-elle visible sous les textes et les surfaces de l'application ?
|
||||
- Le montage contient-il trois plans ou moins, sauf justification écrite ?
|
||||
- Les sons, musiques et images peuvent-ils être publiés légalement ?
|
||||
- Les commandes de laboratoire, messages opérateur et éléments privés sont-ils absents ?
|
||||
- Le fichier porte-t-il le bon ID, la langue et la bonne version ?
|
||||
- Le master a-t-il été regardé intégralement après export ?
|
||||
|
||||
## Publication
|
||||
|
||||
Cadence initiale : cinq vidéos par semaine.
|
||||
|
||||
| Jour | Type conseillé |
|
||||
|---|---|
|
||||
| Lundi | règle simple ou interface |
|
||||
| Mardi | créature ou interaction |
|
||||
| Mercredi | problème / correction |
|
||||
| Vendredi | feature spectaculaire |
|
||||
| Dimanche | chronique calme, monde ou détail absurde |
|
||||
|
||||
Ne pas publier dix épisodes du même module à la suite même s'ils ont été capturés ensemble. Alterner les plateaux donne l'impression d'un monde cohérent plutôt que de neuf mini-campagnes séparées.
|
||||
|
||||
La légende doit compléter la vidéo, pas répéter son texte. Structure recommandée :
|
||||
|
||||
```text
|
||||
Une phrase de contexte.
|
||||
La règle exacte en une phrase.
|
||||
Sanctuary — épisode 041.
|
||||
```
|
||||
|
||||
Limiter les appels à l'action. Une question réelle peut être utilisée lorsqu'elle ouvre le sujet suivant ; éviter les demandes génériques de like ou d'abonnement sur chaque publication.
|
||||
|
||||
## Revue des performances
|
||||
|
||||
Enregistrer à J+1 puis J+7 :
|
||||
|
||||
- vues ;
|
||||
- rétention dans les deux premières secondes ;
|
||||
- durée moyenne regardée ;
|
||||
- taux de complétion ;
|
||||
- relectures ;
|
||||
- partages et enregistrements ;
|
||||
- visites de profil ;
|
||||
- questions ou incompréhensions récurrentes.
|
||||
|
||||
Comparer chaque vidéo à la médiane de son propre format : une chronique de 25 secondes ne doit pas être jugée comme une interaction de huit secondes.
|
||||
|
||||
Décisions :
|
||||
|
||||
- accroche forte et bonne complétion : produire une suite dans les sept jours ;
|
||||
- accroche faible mais bonne complétion chez les spectateurs restants : recouper les deux premières secondes pour un futur sujet proche ;
|
||||
- chute au moment d'une explication : raccourcir ou déplacer la phrase en légende ;
|
||||
- question répétée : créer une vidéo-réponse, sans alourdir la vidéo originale ;
|
||||
- bon sujet mais preuve peu visible : retourner la scène, pas seulement changer le texte ;
|
||||
- résultat faible isolé : ne pas abandonner immédiatement tout le format.
|
||||
|
||||
## Tableau de suivi minimal
|
||||
|
||||
Le suivi peut rester dans un tableur ou un outil Kanban, avec une ligne par ID et ces colonnes :
|
||||
|
||||
| Champ | Usage |
|
||||
|---|---|
|
||||
| `id` | `SNC-TT-001` à `SNC-TT-100` |
|
||||
| `title` | titre canonique du script |
|
||||
| `batch` | `UI`, `WRLD`, `ALPH`, `MOVE`, `PASS`, `HOST`, `FARM`, `TECH` ou `OUCH` |
|
||||
| `status` | état unique du pipeline |
|
||||
| `pack_version` | version réellement capturée |
|
||||
| `source_verified` | oui/non et date |
|
||||
| `scene_ready` | oui/non |
|
||||
| `multiplayer` | oui/non |
|
||||
| `capture_date` | date réelle |
|
||||
| `master_path` | chemin du master approuvé |
|
||||
| `rights_checked` | oui/non |
|
||||
| `scheduled_at` | date de programmation |
|
||||
| `published_url` | URL réelle |
|
||||
| `review_j1` | métriques et note courte |
|
||||
| `review_j7` | métriques et décision |
|
||||
| `follow_up` | nouvel ID ou action décidée |
|
||||
|
||||
## Prochaine action concrète
|
||||
|
||||
Commencer par la semaine 0, puis ne préparer que les douze pilotes. Le système n'est considéré comme verrouillé qu'après :
|
||||
|
||||
1. un export regardé sur téléphone ;
|
||||
2. quatre publications réelles ;
|
||||
3. une revue J+1 de ces quatre vidéos ;
|
||||
4. une décision explicite sur la taille du texte, le rythme, le son et le carton final.
|
||||
|
||||
Après cette validation, les 88 vidéos restantes peuvent entrer en production industrielle sans devoir être remontées à cause d'un défaut de format découvert trop tard.
|
||||
@@ -1,950 +0,0 @@
|
||||
# Sanctuary TikTok — vidéos 001 à 100
|
||||
|
||||
Première saison éditoriale de TikTok sobres et minimalistes présentant Sanctuary comme une réinterprétation personnelle de Minecraft, une règle à la fois. La chaîne de production complète est décrite dans le [plan de réalisation](PRODUCTION_PLAN.md).
|
||||
|
||||
## Contrat de réalisation
|
||||
|
||||
- Format vertical 9:16, 1080 × 1920, 30 images par seconde.
|
||||
- Une seule idée par vidéo, généralement en 10 à 20 secondes.
|
||||
- Trois plans maximum, coupes franches, caméra fixe ou mouvement très lent.
|
||||
- HUD masqué sauf lorsque l'interface constitue la preuve.
|
||||
- Sons du jeu prioritaires ; musique et voix off facultatives.
|
||||
- Deux lignes de texte maximum simultanément.
|
||||
- Carton final de 0,5 à 1 seconde : `SANCTUARY / <SÉRIE> <NUMÉRO>`.
|
||||
- Les valeurs, limites et comportements annoncés doivent rester identiques aux sources liées.
|
||||
- Pour les correctifs, montrer le symptôme puis la correction ; ne jamais fabriquer un faux « avant ».
|
||||
|
||||
Les textes proposés sont une base de tournage. Ils peuvent être lus en voix off ou affichés à l'écran, mais ne doivent pas être doublés intégralement sur les deux canaux.
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
| Épisodes | Axe principal |
|
||||
|---|---|
|
||||
| 001 | Manifeste de la série |
|
||||
| 002–019 | Progression, inventaire et Atlas |
|
||||
| 020–031 | Monde flottant, biomes et Alpha |
|
||||
| 032–039 | Waystones, grappin et poulies |
|
||||
| 040–059 | Creepers, animaux, monstres et familiers |
|
||||
| 060–071 | Agriculture, cuisine et Canaplia |
|
||||
| 072–081 | Temps, météo, lumière, image et audio |
|
||||
| 082–090 | Red-Stoner, stockage et coffre-fort |
|
||||
| 091–096 | Arsenal Ouch |
|
||||
| 097–100 | Shop, marché, quêtes et Only Fun |
|
||||
|
||||
## 001 — Je réinterprète Minecraft, un détail à la fois
|
||||
|
||||
- **Format / durée :** manifeste, 15 s.
|
||||
- **Accroche :** « Je ne voulais pas seulement ajouter des mods à Minecraft. »
|
||||
- **Plans :** îles flottantes au loin ; Creeper portant une fleur ; inventaire qui gagne une nouvelle rangée ; coupe noire.
|
||||
- **Texte / voix :** « Je voulais réinterpréter ses règles, son monde et ses créatures. Un détail à la fois. »
|
||||
- **Sortie :** `SANCTUARY / MANIFESTE 001`.
|
||||
- **Source :** [présentation des dix modules](../../README.md#modules).
|
||||
|
||||
## 002 — Ici, tu commences réellement faible
|
||||
|
||||
- **Format / durée :** une règle, 12 s.
|
||||
- **Accroche :** « Trois cœurs. Trois morceaux de faim. »
|
||||
- **Plans :** apparition sur l'île ; gros plan propre sur le HUD ; tentative de minage volontairement lente.
|
||||
- **Texte / voix :** « Dans Sanctuary, le personnage vanilla complet est quelque chose que tu reconstruis. »
|
||||
- **Sortie :** `SANCTUARY / PROGRESSION 002`.
|
||||
- **Source :** [départ de partie et statistiques initiales](../../sanctuary/PROGRESSION.md#2-départ-de-partie).
|
||||
|
||||
## 003 — Tu ne commences qu'avec ta hotbar
|
||||
|
||||
- **Format / durée :** une règle, 10 s.
|
||||
- **Accroche :** « Au début, ceci est tout ton inventaire. »
|
||||
- **Plans :** ramasser plusieurs objets ; ouvrir l'inventaire ; révéler uniquement les neuf cases disponibles.
|
||||
- **Texte / voix :** « Les autres rangées ne sont pas cachées. Elles ne sont pas encore à toi. »
|
||||
- **Sortie :** `SANCTUARY / INVENTAIRE 003`.
|
||||
- **Source :** [inventaire extensible](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 004 — La place se gagne
|
||||
|
||||
- **Format / durée :** interaction, 14 s.
|
||||
- **Accroche :** « Ton XP peut agrandir ton inventaire. »
|
||||
- **Plans :** inventaire à une rangée ; achat du rang suivant dans Progression ; réouverture avec deux rangées.
|
||||
- **Texte / voix :** « Cinq achats permettent de passer d'une seule rangée à six. Les déblocages restent acquis après la mort. »
|
||||
- **Sortie :** `SANCTUARY / INVENTAIRE 004`.
|
||||
- **Source :** [inventaire extensible et coûts](../../sanctuary/PROGRESSION.md#44-inventaire-extensible-alpha74).
|
||||
|
||||
## 005 — Tab change réellement de hotbar
|
||||
|
||||
- **Format / durée :** interaction cachée, 10 s.
|
||||
- **Accroche :** « Ta deuxième rangée peut devenir ta hotbar. »
|
||||
- **Plans :** objet distinct dans deux rangées ; pression sur `Tab` ; échange visible de la rangée active sans ouvrir de menu.
|
||||
- **Texte / voix :** « `Tab` avance. `Maj + Tab` revient. Le serveur échange la vraie hotbar vanilla. »
|
||||
- **Sortie :** `SANCTUARY / INVENTAIRE 005`.
|
||||
- **Source :** [rangées de hotbar](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 006 — Miner 256 blocs sans tricher
|
||||
|
||||
- **Format / durée :** démonstration, 15 s.
|
||||
- **Accroche :** « Le minage groupé grandit avec ton personnage. »
|
||||
- **Plans :** cible composée de blocs identiques ; maintien de `Mouse 4` et sélection à la molette ; destruction bornée de la veine.
|
||||
- **Texte / voix :** « Les rangs passent de 4 à 256 blocs. Portée, permissions, outils, usure et drops restent vérifiés par le serveur. »
|
||||
- **Sortie :** `SANCTUARY / MINAGE 006`.
|
||||
- **Source :** [rangs de minage groupé](../../sanctuary/PROGRESSION.md#minage--sept-améliorations).
|
||||
|
||||
## 007 — Construire jusqu'à 15 × 15
|
||||
|
||||
- **Format / durée :** démonstration, 15 s.
|
||||
- **Accroche :** « Un clic peut poser un plan entier. »
|
||||
- **Plans :** prévisualisation 3 × 3 ; sélection d'un rang supérieur à la molette ; placement final d'un plan 15 × 15.
|
||||
- **Texte / voix :** « `B` et clic droit construisent de 3 × 3 à 15 × 15. Chaque bloc reste placé par une action Minecraft normale. »
|
||||
- **Sortie :** `SANCTUARY / CONSTRUCTION 007`.
|
||||
- **Source :** [construction groupée](../../sanctuary/PROGRESSION.md#construction--sept-améliorations).
|
||||
|
||||
## 008 — Ta portée devient une statistique
|
||||
|
||||
- **Format / durée :** avant / après, 12 s.
|
||||
- **Accroche :** « Au départ, ce bloc est trop loin. »
|
||||
- **Plans :** interaction refusée à trois blocs dépassés ; achat de Construction ; même caméra, interaction réussie jusqu'au plafond de 5,5 blocs.
|
||||
- **Texte / voix :** « La portée de construction progresse. La portée d'attaque, elle, ne change pas. »
|
||||
- **Sortie :** `SANCTUARY / PROGRESSION 008`.
|
||||
- **Source :** [rangs de Construction](../../sanctuary/PROGRESSION.md#construction--sept-améliorations).
|
||||
|
||||
## 009 — Les bons repas donnent de vrais cœurs temporaires
|
||||
|
||||
- **Format / durée :** une règle, 13 s.
|
||||
- **Accroche :** « Manger peut te protéger sans augmenter ta vie permanente. »
|
||||
- **Plans :** HUD avant le repas ; consommation ; apparition des cœurs orange ; disparition d'une portion après sa durée.
|
||||
- **Texte / voix :** « Nutrition, saturation et effets déterminent jusqu'à trois cœurs de repas pendant 15 à 60 secondes. »
|
||||
- **Sortie :** `SANCTUARY / REPAS 009`.
|
||||
- **Source :** [cœurs de repas](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 010 — Une barre pleine ne suffit pas pour sprinter
|
||||
|
||||
- **Format / durée :** problème / règle, 12 s.
|
||||
- **Accroche :** « Six points de faim. Barre pleine. Sprint impossible. »
|
||||
- **Plans :** tentative de sprint au rang initial ; achat du premier rang de Faim ; sprint réussi avec plus de six points.
|
||||
- **Texte / voix :** « Sanctuary garde le seuil absolu de Minecraft. Ton premier rang de Faim débloque réellement la course. »
|
||||
- **Sortie :** `SANCTUARY / FAIM 010`.
|
||||
- **Source :** [faim et seuil de sprint](../../sanctuary/PROGRESSION.md#faim--sept-améliorations).
|
||||
|
||||
## 011 — Le souffle n'encombre plus l'écran
|
||||
|
||||
- **Format / durée :** avant / après, 10 s.
|
||||
- **Accroche :** « Pourquoi afficher ton air quand tu es sur terre ? »
|
||||
- **Plans :** HUD terrestre sans bulles ; entrée dans l'eau ; apparition de trois à dix bulles selon le rang.
|
||||
- **Texte / voix :** « La réserve de souffle n'apparaît que lorsqu'elle devient utile. »
|
||||
- **Sortie :** `SANCTUARY / HUD 011`.
|
||||
- **Source :** [HUD de souffle](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 012 — L'expérience achète des capacités permanentes
|
||||
|
||||
- **Format / durée :** une règle, 14 s.
|
||||
- **Accroche :** « Dans Sanctuary, l'XP ne sert pas qu'aux enchantements. »
|
||||
- **Plans :** niveaux disponibles ; écran Progression ; achat du rang suivant ; mort rapide puis retour du rang.
|
||||
- **Texte / voix :** « Vie, faim, minage, souffle, construction et inventaire progressent avec les niveaux vanilla. Les achats survivent à la mort. »
|
||||
- **Sortie :** `SANCTUARY / PROGRESSION 012`.
|
||||
- **Source :** [améliorations et récompenses](../../sanctuary/PROGRESSION.md#4-améliorations-et-récompenses).
|
||||
|
||||
## 013 — La progression appartient au serveur
|
||||
|
||||
- **Format / durée :** note de laboratoire, 16 s.
|
||||
- **Accroche :** « Le client demande. Le serveur décide. »
|
||||
- **Plans :** achat valide ; tentative sans assez d'XP ; action refusée sans désynchronisation.
|
||||
- **Texte / voix :** « Le prix n'est jamais choisi par le client. Le serveur vérifie le rang, le solde et applique chaque capacité. »
|
||||
- **Sortie :** `SANCTUARY / LABO 013`.
|
||||
- **Source :** [contrat serveur de progression](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 014 — Une carte composée de 160 000 points
|
||||
|
||||
- **Format / durée :** révélation, 15 s.
|
||||
- **Accroche :** « L'Atlas n'est pas une image vue du ciel. »
|
||||
- **Plans :** ouverture avec `M` ; rotation lente du nuage de points ; zoom sur le relief d'un chunk.
|
||||
- **Texte / voix :** « C'est une vue GPU orientable, construite depuis la hauteur et la couleur du terrain exploré. »
|
||||
- **Sortie :** `SANCTUARY / ATLAS 014`.
|
||||
- **Source :** [Atlas en nuage de points](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 015 — Le monde calculé n'est pas le monde découvert
|
||||
|
||||
- **Format / durée :** une règle, 14 s.
|
||||
- **Accroche :** « L'Atlas connaît les biomes. Pas tes souvenirs. »
|
||||
- **Plans :** fond de biomes discret ; marche vers une zone inconnue ; apparition du profil détaillé après chargement du chunk.
|
||||
- **Texte / voix :** « Le fog of war sépare le monde prévisible des lieux que tu as réellement visités. »
|
||||
- **Sortie :** `SANCTUARY / ATLAS 015`.
|
||||
- **Source :** [fog of war persistant](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 016 — La carte peut exister dans le monde
|
||||
|
||||
- **Format / durée :** interaction, 12 s.
|
||||
- **Accroche :** « Maintiens `Mouse 5`. »
|
||||
- **Plans :** vue FPS normale ; apparition de la projection Atlas dans le monde ; regard vers l'horizon et disparition progressive.
|
||||
- **Texte / voix :** « Son opacité suit ton regard pour ne pas masquer le paysage. »
|
||||
- **Sortie :** `SANCTUARY / ATLAS 016`.
|
||||
- **Source :** [projection temps réel de l'Atlas](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 017 — Une mini-carte à neuf positions
|
||||
|
||||
- **Format / durée :** interaction cachée, 13 s.
|
||||
- **Accroche :** « La même carte peut devenir une mini-carte. »
|
||||
- **Plans :** activation avec `Mouse 5` ; déplacement entre trois des neuf ancrages ; réglage du zoom à la molette.
|
||||
- **Texte / voix :** « Permanente, maintenue ou désactivée. Placée là où ton interface en a besoin. »
|
||||
- **Sortie :** `SANCTUARY / ATLAS 017`.
|
||||
- **Source :** [modes et placements de mini-carte](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 018 — L'Atlas observe les traces du monde
|
||||
|
||||
- **Format / durée :** découverte, 15 s.
|
||||
- **Accroche :** « Certains lieux se cartographient sans marqueur manuel. »
|
||||
- **Plans :** passage près d'un coffre, d'un portail et d'un générateur ; ouverture de l'Atlas ; trois pictogrammes fixes visibles.
|
||||
- **Texte / voix :** « Coffres, portails, générateurs et rails souterrains deviennent des points d'intérêt locaux. »
|
||||
- **Sortie :** `SANCTUARY / ATLAS 018`.
|
||||
- **Source :** [détection des lieux Atlas](../../README.md#progression-sanctuary-alpha).
|
||||
|
||||
## 019 — Maison et Arbre ne sont jamais devinés
|
||||
|
||||
- **Format / durée :** une règle, 14 s.
|
||||
- **Accroche :** « Sanctuary refuse de décider où se trouve ta maison. »
|
||||
- **Plans :** Personal Computer posé ; clic sur « Maison ici » ; apparition du pictogramme et de son libellé dans l'Atlas.
|
||||
- **Texte / voix :** « Ces lieux sont personnels. Tu les déclares toi-même depuis ton ordinateur. »
|
||||
- **Sortie :** `SANCTUARY / ATLAS 019`.
|
||||
- **Source :** [Personal Computer et lieux personnels](../../README.md#red-stoner-et-photographie).
|
||||
|
||||
## 020 — Le monde principal est un archipel flottant
|
||||
|
||||
- **Format / durée :** chronique du monde, 18 s.
|
||||
- **Accroche :** aucun texte pendant les deux premières secondes.
|
||||
- **Plans :** départ sur l'île centrale ; travelling très lent au-dessus de la couronne de vide ; révélation des îles procédurales extérieures.
|
||||
- **Texte / voix :** « Sanctuary remplace le monde par défaut : un refuge central, du vide, puis un archipel sans fin. »
|
||||
- **Sortie :** `SANCTUARY / MONDE 020`.
|
||||
- **Source :** [génération du monde principal](../../README.md#génération-du-monde-principal).
|
||||
|
||||
## 021 — Le vide protège le début de partie
|
||||
|
||||
- **Format / durée :** une règle du monde, 15 s.
|
||||
- **Accroche :** « Cette absence de terrain est volontaire. »
|
||||
- **Plans :** bord de l'île ; longue focale sur le vide ; îles lointaines reprenant au-delà de la couronne.
|
||||
- **Texte / voix :** « Le refuge initial est séparé du monde procédural. S'en échapper devient une étape de progression. »
|
||||
- **Sortie :** `SANCTUARY / MONDE 021`.
|
||||
- **Source :** [départ de partie](../../sanctuary/PROGRESSION.md#2-départ-de-partie).
|
||||
|
||||
## 022 — 108 biomes, sans charger Terralith
|
||||
|
||||
- **Format / durée :** preuve visuelle, 18 s.
|
||||
- **Accroche :** « Sanctuary distribue 108 biomes natifs. »
|
||||
- **Plans :** trois biomes très contrastés en coupes franches ; survol d'une transition ; écran final sobre avec `108`.
|
||||
- **Texte / voix :** « Huit biomes historiques et cent créations Terramix, générés sous les règles climatiques natives du pack. »
|
||||
- **Sortie :** `SANCTUARY / BIOMES 022`.
|
||||
- **Source :** [biomes natifs d'Another World](../../anotherworld/README.md#biomes-natifs).
|
||||
|
||||
## 023 — Le climat dessine des provinces
|
||||
|
||||
- **Format / durée :** note de conception, 17 s.
|
||||
- **Accroche :** « Les biomes ne sont pas tirés au hasard chunk par chunk. »
|
||||
- **Plans :** travelling continu traversant une zone chaude et sèche ; transition vers une zone humide ; carte large montrant les grands contours.
|
||||
- **Texte / voix :** « Température et humidité forment de grandes provinces. Une variation locale casse ensuite leurs frontières. »
|
||||
- **Sortie :** `SANCTUARY / CLIMAT 023`.
|
||||
- **Source :** [prototype climatique de l'alpha.189](../../CHANGELOG.md#2620-alpha189).
|
||||
|
||||
## 024 — Les Sulfur Caves s'annoncent dans la pierre
|
||||
|
||||
- **Format / durée :** découverte, 15 s.
|
||||
- **Accroche :** « Avant la caverne, la roche te prévient. »
|
||||
- **Plans :** tunnel de deepslate ; première amorce de soufre ; révélation des piscines et pointes de la Sulfur Cave.
|
||||
- **Texte / voix :** « Une trace visible précède la formation principale. L'exploration peut se lire dans les parois. »
|
||||
- **Sortie :** `SANCTUARY / CAVERNES 024`.
|
||||
- **Source :** [Sulfur Caves de l'alpha.179](../../CHANGELOG.md#2620-alpha179).
|
||||
|
||||
## 025 — Un donjon peut être suspendu sous une île
|
||||
|
||||
- **Format / durée :** chronique du monde, 17 s.
|
||||
- **Accroche :** « Certains donjons n'ont pas de sol. »
|
||||
- **Plans :** île flottante vue de côté ; descente verticale ; révélation d'un donjon de cobblestone accroché par des chaînes.
|
||||
- **Texte / voix :** « S'il trouve de la matière au-dessus, le donjon s'y suspend. Sans ancrage, il n'apparaît pas. »
|
||||
- **Sortie :** `SANCTUARY / DONJONS 025`.
|
||||
- **Source :** [donjons suspendus de l'alpha.188](../../CHANGELOG.md#2620-alpha188).
|
||||
|
||||
## 026 — Une ravine tapissée de seize couleurs
|
||||
|
||||
- **Format / durée :** révélation, 14 s.
|
||||
- **Accroche :** « Ce biome est volontairement presque introuvable. »
|
||||
- **Plans :** marche dans un paysage sombre ; arrivée au bord ; plongée lente dans une Rainbow Ravine verticale.
|
||||
- **Texte / voix :** « Les seize blocs arc-en-ciel suivent les entailles irrégulières du relief, du sommet jusqu'au fond. »
|
||||
- **Sortie :** `SANCTUARY / BIOMES 026`.
|
||||
- **Source :** [Rainbow Ravine de l'alpha.114](../../CHANGELOG.md#2620-alpha114).
|
||||
|
||||
## 027 — Un monde Minecraft fini en 128 × 128 × 64
|
||||
|
||||
- **Format / durée :** chronique historique, 18 s.
|
||||
- **Accroche :** « Ce monde possède une vraie fin. »
|
||||
- **Plans :** panorama de l'île Indev ; plage, caverne et maison en coupes courtes ; approche de la limite extérieure.
|
||||
- **Texte / voix :** « Alpha reconstruit un volume déterministe de 128 × 128 × 64 blocs. Rien ne se génère au-delà. »
|
||||
- **Sortie :** `SANCTUARY / ALPHA 027`.
|
||||
- **Source :** [contrat de génération Alpha](../../sanctuary/ALPHA.md#contrat-de-génération).
|
||||
|
||||
## 028 — Le portail d'Alpha se construit en cobblestone moussue
|
||||
|
||||
- **Format / durée :** interaction, 15 s.
|
||||
- **Accroche :** « Quatre sur cinq. Deux sur trois à l'intérieur. »
|
||||
- **Plans :** construction du cadre extérieur 4 × 5 ; allumage au briquet ; arrivée sûre hors de la maison Indev.
|
||||
- **Texte / voix :** « Le serveur valide le cadre, la dimension et l'arrivée. La première traversée crée une paire persistante. »
|
||||
- **Sortie :** `SANCTUARY / PORTAIL 028`.
|
||||
- **Source :** [accès à Alpha](../../sanctuary/ALPHA.md#accès).
|
||||
|
||||
## 029 — Les textures Alpha n'existent que dans Alpha
|
||||
|
||||
- **Format / durée :** avant / après, 16 s.
|
||||
- **Accroche :** « Un resource pack peut appartenir à une dimension. »
|
||||
- **Plans :** monde normal avec la pile habituelle ; traversée vers Alpha et rechargement ; retour et restauration exacte de l'apparence précédente.
|
||||
- **Texte / voix :** « Ambiance compose Golden Days Base puis Alpha uniquement pendant la visite, sans redistribuer leurs fichiers. »
|
||||
- **Sortie :** `SANCTUARY / AMBIANCE 029`.
|
||||
- **Source :** [composition Golden Days](../../ambiance/GOLDEN_DAYS.md).
|
||||
|
||||
## 030 — Notch habite encore dans la maison Indev
|
||||
|
||||
- **Format / durée :** chronique, 16 s.
|
||||
- **Accroche :** « Dans ce monde, Notch est un résident. Pas un spawn aléatoire. »
|
||||
- **Plans :** approche silencieuse de la maison ; Notch à l'intérieur ; même maison après un redémarrage de la scène.
|
||||
- **Texte / voix :** « Il existe en un seul exemplaire persistant. S'il disparaît, Sanctuary sait le ramener chez lui. »
|
||||
- **Sortie :** `SANCTUARY / ALPHA 030`.
|
||||
- **Source :** [résident d'Alpha](../../sanctuary/ALPHA.md#résident-et-jalon-mondial).
|
||||
|
||||
## 031 — La première victoire crée un seul Magic Carpet
|
||||
|
||||
- **Format / durée :** récompense, 18 s.
|
||||
- **Accroche :** « Ce coffre n'existe qu'après la première victoire mondiale. »
|
||||
- **Plans :** fin du combat de Notch ; feu d'artifice près de la maison ; ouverture du coffre avec le tapis placé au centre.
|
||||
- **Texte / voix :** « La réservation et la livraison sont sauvegardées séparément : même un redémarrage ne duplique pas la récompense. »
|
||||
- **Sortie :** `SANCTUARY / RÉCOMPENSE 031`.
|
||||
- **Source :** [première récompense du Magic Carpet](../../iliketomoveit/MAGIC_CARPET.md#première-récompense).
|
||||
|
||||
## 032 — Une Waystone doit être découverte physiquement
|
||||
|
||||
- **Format / durée :** une règle, 13 s.
|
||||
- **Accroche :** « Poser une pierre ne suffit pas pour la connaître. »
|
||||
- **Plans :** menu sans destination ; arrivée à pied et contact avec la pierre ; destination maintenant visible.
|
||||
- **Texte / voix :** « Chaque joueur possède son propre réseau de lieux réellement visités. »
|
||||
- **Sortie :** `SANCTUARY / WAYSTONE 032`.
|
||||
- **Source :** [Waystones locales](../../README.md#waystones-locaux).
|
||||
|
||||
## 033 — Le voyage coûte la distance parcourue
|
||||
|
||||
- **Format / durée :** une règle, 12 s.
|
||||
- **Accroche :** « Une téléportation lointaine ne coûte pas comme un saut de voisinage. »
|
||||
- **Plans :** deux destinations à des distances différentes ; affichage des coûts ; voyage vers la plus lointaine.
|
||||
- **Texte / voix :** « Le serveur facture un niveau d'XP par tranche commencée de 512 blocs et recherche une arrivée sûre. »
|
||||
- **Sortie :** `SANCTUARY / WAYSTONE 033`.
|
||||
- **Source :** [coût des Waystones](../../README.md#waystones-locaux).
|
||||
|
||||
## 034 — Une pierre peut être renommée sans commande
|
||||
|
||||
- **Format / durée :** interaction cachée, 10 s.
|
||||
- **Accroche :** « Ouvre une Waystone. Appuie sur `R`. »
|
||||
- **Plans :** ouverture du réseau ; saisie d'un nom court ; validation avec `Entrée` puis affichage immédiat.
|
||||
- **Texte / voix :** « `Échap` annule. Le nouveau nom reste attaché à la pierre. »
|
||||
- **Sortie :** `SANCTUARY / WAYSTONE 034`.
|
||||
- **Source :** [renommage des Waystones](../../README.md#waystones-locaux).
|
||||
|
||||
## 035 — Un ascenseur peut descendre vers l'inconnu
|
||||
|
||||
- **Format / durée :** interaction cachée, 13 s.
|
||||
- **Accroche :** « Tu peux descendre vers un étage que tu n'as jamais visité. »
|
||||
- **Plans :** deux Waystones alignées en X/Z ; accroupissement sur celle du haut ; arrivée en bas et découverte de la destination.
|
||||
- **Texte / voix :** « La montée reste interdite tant que l'étage supérieur n'a pas été découvert. »
|
||||
- **Sortie :** `SANCTUARY / ASCENSEUR 035`.
|
||||
- **Source :** [ascenseur vertical Waystone](../../README.md#waystones-locaux).
|
||||
|
||||
## 036 — Le grappin conserve ton élan
|
||||
|
||||
- **Format / durée :** démonstration, 14 s.
|
||||
- **Accroche :** « Ce grappin ne te téléporte pas vers son crochet. »
|
||||
- **Plans :** lancement ; balancement en arc lisible ; lâcher au point bas et conservation de la vitesse.
|
||||
- **Texte / voix :** « Charge, longueur de corde, rappel, descente et restitution de l'élan composent le mouvement. »
|
||||
- **Sortie :** `SANCTUARY / GRAPPIN 036`.
|
||||
- **Source :** [grappin et poulies](../../README.md#grappin-et-poulies).
|
||||
|
||||
## 037 — Un grappin fluide, même quand le serveur décide
|
||||
|
||||
- **Format / durée :** problème / Sanctuary, 16 s.
|
||||
- **Accroche :** « En multijoueur, un grappin peut devenir une suite de corrections. »
|
||||
- **Plans :** capture authentique d'un ancien mouvement saccadé si disponible ; version actuelle fluide ; très bref arrêt sur la position réconciliée.
|
||||
- **Texte / voix :** « Le client prédit le même pendule. Le serveur reste l'autorité et ne corrige que dans une limite bornée. »
|
||||
- **Sortie :** `SANCTUARY / LABO 037`.
|
||||
- **Source :** [prédiction du grappin, alpha.196](../../CHANGELOG.md#2620-alpha196).
|
||||
|
||||
## 038 — Deux poulies créent une tyrolienne
|
||||
|
||||
- **Format / durée :** démonstration, 12 s.
|
||||
- **Accroche :** « Deux points. Jusqu'à 128 blocs. »
|
||||
- **Plans :** liaison des deux têtes ; vue latérale de la ligne ; traversée complète à vitesse constante.
|
||||
- **Texte / voix :** « Les collisions sont vérifiées pendant tout le trajet. »
|
||||
- **Sortie :** `SANCTUARY / POULIE 038`.
|
||||
- **Source :** [poulies et tyrolienne](../../README.md#grappin-et-poulies).
|
||||
|
||||
## 039 — La tyrolienne peut piloter une machine
|
||||
|
||||
- **Format / durée :** interaction entre systèmes, 12 s.
|
||||
- **Accroche :** « Le passage d'un joueur devient un signal. »
|
||||
- **Plans :** lampe redstone éteinte reliée à la poulie ; joueur traversant ; signal 15 et lampe allumée pendant le passage.
|
||||
- **Texte / voix :** « Le déplacement peut devenir une entrée d'automatisation. »
|
||||
- **Sortie :** `SANCTUARY / SYSTÈMES 039`.
|
||||
- **Source :** [signal redstone des poulies](../../README.md#grappin-et-poulies).
|
||||
|
||||
## 040 — Un Creeper peut naître avec sept apparences
|
||||
|
||||
- **Format / durée :** collection, 15 s.
|
||||
- **Accroche :** « C'est toujours le Creeper vanilla. »
|
||||
- **Plans :** même cadre, coupes rapides entre classic, simple, pig, business, better, shiny et starry night ; arrêt sur le dernier.
|
||||
- **Texte / voix :** « L'entité, la charge et l'œuf restent vanilla. L'apparence et le tempérament deviennent persistants. »
|
||||
- **Sortie :** `SANCTUARY / CREEPER 040`.
|
||||
- **Source :** [apparences de Creepers](../../itsalive/README.md#creepers).
|
||||
|
||||
## 041 — Une fleur peut rendre un Creeper heureux
|
||||
|
||||
- **Format / durée :** interaction, 11 s.
|
||||
- **Accroche :** « Essaie de lui donner une fleur. »
|
||||
- **Plans :** approche dangereuse ; remise de la fleur ; fleur visible sur la tête et Creeper suivant le joueur.
|
||||
- **Texte / voix :** « Le changement n'est pas cosmétique : son tempérament devient heureux. »
|
||||
- **Sortie :** `SANCTUARY / CREEPER 041`.
|
||||
- **Source :** [tempérament heureux](../../itsalive/README.md#creepers).
|
||||
|
||||
## 042 — Le bonheur d'un Creeper est réversible
|
||||
|
||||
- **Format / durée :** interaction cachée, 10 s.
|
||||
- **Accroche :** « Tu peux reprendre la fleur. »
|
||||
- **Plans :** Creeper heureux ; tonte ; fleur rendue au joueur et retour immédiat du tempérament dangereux.
|
||||
- **Texte / voix :** « Rien n'est supprimé silencieusement. Même la fleur revient. »
|
||||
- **Sortie :** `SANCTUARY / CREEPER 042`.
|
||||
- **Source :** [tonte du Creeper](../../itsalive/README.md#creepers).
|
||||
|
||||
## 043 — Une poule sur 256 peut pondre ceci
|
||||
|
||||
- **Format / durée :** découverte rare, 12 s.
|
||||
- **Accroche :** plan fixe sur l'œuf doré, sans texte pendant une seconde.
|
||||
- **Plans :** poule vanilla ; ponte d'un œuf doré mise en scène avec une capture authentique ou une reproduction de laboratoire clairement identifiée ; lancer de l'œuf.
|
||||
- **Texte / voix :** « Une poule vanilla possède une chance sur 256 de pondre l'entrée vers une nouvelle lignée. »
|
||||
- **Sortie :** `SANCTUARY / ŒUF DORÉ 043`.
|
||||
- **Source :** [œuf doré et poules à gemmes](../../itsalive/README.md#poules-à-gemmes).
|
||||
|
||||
## 044 — Les poules peuvent pondre des gemmes
|
||||
|
||||
- **Format / durée :** collection, 15 s.
|
||||
- **Accroche :** « Verte, rouge ou bleue. Trois cadences différentes. »
|
||||
- **Plans :** bébé issu de l'œuf doré ; trois variantes adultes ; émeraude, rubis et saphir pondus en coupes distinctes.
|
||||
- **Texte / voix :** « L'émeraude est la plus rapide. Le saphir, la plus lente. »
|
||||
- **Sortie :** `SANCTUARY / POULLES 044`.
|
||||
- **Source :** [variantes et cadences des poules à gemmes](../../itsalive/README.md#poules-à-gemmes).
|
||||
|
||||
## 045 — Une vache sur 64 se tient debout
|
||||
|
||||
- **Format / durée :** découverte absurde, 11 s.
|
||||
- **Accroche :** plan fixe d'une vache normale qui sort du cadre, puis révélation de la vache debout.
|
||||
- **Plans :** contraste des deux silhouettes ; marche de la variante ; plan final frontal.
|
||||
- **Texte / voix :** « La mutation est rare, persistante et peut se transmettre par l'élevage. »
|
||||
- **Sortie :** `SANCTUARY / VACHE 045`.
|
||||
- **Source :** [vaches debout rares](../../itsalive/README.md#vaches-debout-rares).
|
||||
|
||||
## 046 — Chaque villageois reçoit un nom stable
|
||||
|
||||
- **Format / durée :** une règle, 12 s.
|
||||
- **Accroche :** « Ce villageois ne changera pas de nom au redémarrage. »
|
||||
- **Plans :** trois villageois avec noms différents ; sortie puis retour dans le monde ; mêmes noms visibles.
|
||||
- **Texte / voix :** « Le nom vient de son UUID. Un nom donné manuellement n'est jamais remplacé. »
|
||||
- **Sortie :** `SANCTUARY / VILLAGE 046`.
|
||||
- **Source :** [villageois nommés](../../itsalive/README.md#villageois-nommés).
|
||||
|
||||
## 047 — Il existe seize Mooblooms
|
||||
|
||||
- **Format / durée :** collection, 16 s.
|
||||
- **Accroche :** « Une seule créature. Seize fleurs. »
|
||||
- **Plans :** travelling devant une ligne de variantes ; gros plan sur les fleurs du dos ; gros plan sur celle qui suit la tête.
|
||||
- **Texte / voix :** « Chaque variante est synchronisée, sauvegardée et possède son propre œuf de test. »
|
||||
- **Sortie :** `SANCTUARY / MOOBLOOM 047`.
|
||||
- **Source :** [seize variantes de Moobloom](../../itsalive/README.md#mooblooms).
|
||||
|
||||
## 048 — Un veau Moobloom hérite d'un de ses parents
|
||||
|
||||
- **Format / durée :** expérience, 13 s.
|
||||
- **Accroche :** « Deux fleurs différentes. Une seule pour le veau. »
|
||||
- **Plans :** deux parents de variantes distinctes ; reproduction ; révélation de la variante héritée.
|
||||
- **Texte / voix :** « Le veau choisit aléatoirement la fleur de l'un de ses deux parents. »
|
||||
- **Sortie :** `SANCTUARY / HÉRITAGE 048`.
|
||||
- **Source :** [élevage des Mooblooms](../../itsalive/README.md#mooblooms).
|
||||
|
||||
## 049 — Les papillons se nourrissent de fleurs
|
||||
|
||||
- **Format / durée :** chronique calme, 14 s.
|
||||
- **Accroche :** aucun texte pendant le premier vol.
|
||||
- **Plans :** groupe de papillons autour d'un biome fleuri ; joueur tenant une fleur ; reproduction puis réaction agressive après une attaque.
|
||||
- **Texte / voix :** « Ils suivent les fleurs, se reproduisent avec elles et se défendent lorsqu'on les attaque. »
|
||||
- **Sortie :** `SANCTUARY / PAPILLONS 049`.
|
||||
- **Source :** [papillons](../../itsalive/README.md#papillons).
|
||||
|
||||
## 050 — Le Ghost ne reste jamais jusqu'au matin
|
||||
|
||||
- **Format / durée :** horreur minimale, 15 s.
|
||||
- **Accroche :** murmure hors champ, sans texte.
|
||||
- **Plans :** Ghost traversant un mur ; charge courte vers le joueur ; lever du jour et disparition.
|
||||
- **Texte / voix :** « La nuit, il hante, murmure, frappe, puis disparaît. Le jour le supprime. »
|
||||
- **Sortie :** `SANCTUARY / GHOST 050`.
|
||||
- **Source :** [Real Ghost](../../itsalive/README.md#real-ghost).
|
||||
|
||||
## 051 — Bracken recule quand tu le regardes
|
||||
|
||||
- **Format / durée :** horreur minimale, 15 s.
|
||||
- **Accroche :** « Ne détourne pas les yeux. Mais ne le fixe pas trop longtemps. »
|
||||
- **Plans :** Bracken approchant dans le dos ; regard du joueur et recul ; rage après un regard prolongé.
|
||||
- **Texte / voix :** « Il préfère attaquer par derrière. Les haches et le feu lui font davantage de dégâts. »
|
||||
- **Sortie :** `SANCTUARY / BRACKEN 051`.
|
||||
- **Source :** [comportement de Bracken](../../itsalive/README.md#real-ghost).
|
||||
|
||||
## 052 — Sanctuary supprime les Phantoms
|
||||
|
||||
- **Format / durée :** une règle, 9 s.
|
||||
- **Accroche :** « Les Phantoms n'existent pas ici. »
|
||||
- **Plans :** nuit après plusieurs jours sans dormir ; ciel vide ; tentative de chargement d'un Phantom immédiatement supprimé en laboratoire.
|
||||
- **Texte / voix :** « C'est la politique héritée de Real Ghost et No Phantoms. »
|
||||
- **Sortie :** `SANCTUARY / RÈGLE 052`.
|
||||
- **Source :** [suppression des Phantoms](../../itsalive/README.md#real-ghost).
|
||||
|
||||
## 053 — Une baleine de 36 blocs traverse le ciel
|
||||
|
||||
- **Format / durée :** chronique du monde, 18 s.
|
||||
- **Accroche :** garder les deux premières secondes entièrement silencieuses.
|
||||
- **Plans :** ombre passant sur le terrain ; révélation latérale de la baleine ; plan large avec son sillage nuageux.
|
||||
- **Texte / voix :** « Elle est géante, intangible, invulnérable et ne donne aucun butin. Elle existe seulement pour traverser le monde. »
|
||||
- **Sortie :** `SANCTUARY / BALEINE 053`.
|
||||
- **Source :** [baleine volante géante](../../itsalive/README.md#baleine-volante-géante).
|
||||
|
||||
## 054 — Cinq Infectés, une seule entité persistante
|
||||
|
||||
- **Format / durée :** collection, 15 s.
|
||||
- **Accroche :** « Rouge, jaune, orange, violet, vert. »
|
||||
- **Plans :** ligne des cinq variantes ; gros plans sur les t-shirts ; attaque simultanée puis coupe noire.
|
||||
- **Texte / voix :** « Infecté, vomiteur, cracheur, hunter et chargeur partagent la même base, mais pas le même comportement. »
|
||||
- **Sortie :** `SANCTUARY / INFECTÉS 054`.
|
||||
- **Source :** [cinq variantes d'Infectés](../../itsalive/README.md#infectés).
|
||||
|
||||
## 055 — Le Vomiteur transforme la vision en danger
|
||||
|
||||
- **Format / durée :** démonstration, 13 s.
|
||||
- **Accroche :** attaque reçue avant l'apparition du texte.
|
||||
- **Plans :** Vomiteur ; overlay de projections ; désorientation et monstres proches attirés vers le joueur.
|
||||
- **Texte / voix :** « Neuf secondes de vision masquée. Douze secondes de désorientation. Et le bruit attire les monstres. »
|
||||
- **Sortie :** `SANCTUARY / INFECTÉS 055`.
|
||||
- **Source :** [attaque du Vomiteur](../../itsalive/README.md#infectés).
|
||||
|
||||
## 056 — Le Cracheur combat à distance
|
||||
|
||||
- **Format / durée :** démonstration, 11 s.
|
||||
- **Accroche :** « Celui-ci n'a pas besoin de te toucher. »
|
||||
- **Plans :** projectile acide en trajectoire ; impact ; effets Poison et Lenteur visibles.
|
||||
- **Texte / voix :** « Une boule d'acide blesse, empoisonne et ralentit. »
|
||||
- **Sortie :** `SANCTUARY / INFECTÉS 056`.
|
||||
- **Source :** [attaque du Cracheur](../../itsalive/README.md#infectés).
|
||||
|
||||
## 057 — Quatre pressions pour repousser le Hunter
|
||||
|
||||
- **Format / durée :** règle de survie, 16 s.
|
||||
- **Accroche :** écran noir au moment de la capture.
|
||||
- **Plans :** bond corrigé vers une cible mobile ; Hunter assis sur la tête ; quatre pressions distinctes d'accroupissement puis libération.
|
||||
- **Texte / voix :** « Il verrouille sa victime pendant six secondes. Résister quatre fois le repousse. »
|
||||
- **Sortie :** `SANCTUARY / INFECTÉS 057`.
|
||||
- **Source :** [capture du Hunter](../../itsalive/README.md#infectés).
|
||||
|
||||
## 058 — Le Chargeur devient vulnérable après l'impact
|
||||
|
||||
- **Format / durée :** règle de combat, 12 s.
|
||||
- **Accroche :** « Esquiver est aussi une attaque. »
|
||||
- **Plans :** préparation lisible ; charge et esquive latérale ; courte récupération pendant laquelle le joueur riposte.
|
||||
- **Texte / voix :** « Il percute et repousse. Puis il doit récupérer. »
|
||||
- **Sortie :** `SANCTUARY / INFECTÉS 058`.
|
||||
- **Source :** [comportement du Chargeur](../../itsalive/README.md#infectés).
|
||||
|
||||
## 059 — Un œuf équipé devient un pouvoir
|
||||
|
||||
- **Format / durée :** manifeste de système, 16 s.
|
||||
- **Accroche :** « Les œufs d'apparition ne servent plus seulement à faire apparaître des créatures. »
|
||||
- **Plans :** œuf équipé dans le slot de familier ; apparition du familier visuel ; activation avec `G` de trois pouvoirs contrastés en coupes rapides.
|
||||
- **Texte / voix :** « Une essence à la fois. Un passif. Un actif choisi et validé par le serveur. »
|
||||
- **Sortie :** `SANCTUARY / FAMILIERS 059`.
|
||||
- **Source :** [contrat des pouvoirs de familiers](../../sanctuary/COMPANION_POWERS.md#contrat-de-jeu).
|
||||
|
||||
## 060 — Certaines cultures poussent sur deux blocs
|
||||
|
||||
- **Format / durée :** time-lapse, 12 s.
|
||||
- **Accroche :** « Le maïs ne reste pas une texture plate. »
|
||||
- **Plans :** plant bas ; accéléré des huit stades ; maïs, tomate ou concombre devenant réellement haut de deux blocs.
|
||||
- **Texte / voix :** « La moitié supérieure reste synchronisée avec le plant inférieur. »
|
||||
- **Sortie :** `SANCTUARY / AGRICULTURE 060`.
|
||||
- **Source :** [agriculture It's Alive](../../itsalive/README.md#agriculture).
|
||||
|
||||
## 061 — Récolter sans arracher la plante
|
||||
|
||||
- **Format / durée :** interaction, 11 s.
|
||||
- **Accroche :** « Certaines récoltes sont pérennes. »
|
||||
- **Plans :** plant mûr de tomate ; clic de récolte donnant les fruits ; retour visuel du plant à un stade plus jeune.
|
||||
- **Texte / voix :** « Fraise, tomate, concombre, poivron, aubergine et raisin repoussent après la récolte. »
|
||||
- **Sortie :** `SANCTUARY / AGRICULTURE 061`.
|
||||
- **Source :** [cultures pérennes](../../itsalive/README.md#agriculture).
|
||||
|
||||
## 062 — Le riz exige une vraie rizière
|
||||
|
||||
- **Format / durée :** une règle, 12 s.
|
||||
- **Accroche :** « Pas de riz sur une simple terre labourée. »
|
||||
- **Plans :** plantation refusée sur une parcelle ordinaire ; source d'eau pleine posée sur de la boue ; croissance complète du riz immergé.
|
||||
- **Texte / voix :** « Le décor et la règle agricole racontent la même chose. »
|
||||
- **Sortie :** `SANCTUARY / CUISINE 062`.
|
||||
- **Source :** [riz immergé](../../itsalive/FOOD_WIKI.md#riz--une-vraie-rizière).
|
||||
|
||||
## 063 — Le haricot change selon le moment de récolte
|
||||
|
||||
- **Format / durée :** comparaison, 12 s.
|
||||
- **Accroche :** « Récolté jeune ou récolté sec, ce n'est pas le même ingrédient. »
|
||||
- **Plans :** récolte d'un plant jeune ; même cadrage sur le plant mûr ; comparaison des deux objets.
|
||||
- **Texte / voix :** « Le temps de récolte devient un choix culinaire. »
|
||||
- **Sortie :** `SANCTUARY / CUISINE 063`.
|
||||
- **Source :** [haricot vert ou sec](../../itsalive/FOOD_WIKI.md#haricot--vert-ou-sec).
|
||||
|
||||
## 064 — Six machines, six gestes culinaires
|
||||
|
||||
- **Format / durée :** collection, 18 s.
|
||||
- **Accroche :** « Presser. Moudre. Poêler. Bouillir. Cuire. Fermenter. »
|
||||
- **Plans :** une action lisible par poste, toujours avec la même caméra et la même table de travail ; résultat final aligné.
|
||||
- **Texte / voix :** « La cuisine de Sanctuary n'est pas un unique menu de recettes. »
|
||||
- **Sortie :** `SANCTUARY / CUISINE 064`.
|
||||
- **Source :** [postes de cuisine](../../itsalive/FOOD_WIKI.md#postes-de-cuisine).
|
||||
|
||||
## 065 — Le livre culinaire possède 64 pages à découvrir
|
||||
|
||||
- **Format / durée :** découverte, 15 s.
|
||||
- **Accroche :** « Les recettes ne sont pas toutes connues au départ. »
|
||||
- **Plans :** livre presque vide ; obtention d'une page ; nouvelle recette révélée ; livre posé sur un pupitre et lu par un autre joueur.
|
||||
- **Texte / voix :** « Le catalogue est personnel dans l'inventaire et temporairement partageable sur un pupitre. »
|
||||
- **Sortie :** `SANCTUARY / LIVRE 065`.
|
||||
- **Source :** [64 pages et partage du livre](../../itsalive/FOOD_WIKI.md#les-64-pages-de-recettes).
|
||||
|
||||
## 066 — Canaplia possède seize familles
|
||||
|
||||
- **Format / durée :** collection, 16 s.
|
||||
- **Accroche :** « Seize plantes. Seize découvertes. Seize effets. »
|
||||
- **Plans :** grille ou travelling de cultures matures ; trois têtes et résines en gros plan ; Livre interdit refermé.
|
||||
- **Texte / voix :** « Chaque famille possède sa culture, sa progression et son profil serveur. »
|
||||
- **Sortie :** `SANCTUARY / CANAPLIA 066`.
|
||||
- **Source :** [migration Canaplia](../../CANAPLIA.md).
|
||||
|
||||
## 067 — Le climat décide où découvrir Canaplia
|
||||
|
||||
- **Format / durée :** exploration, 15 s.
|
||||
- **Accroche :** « Toutes les plantes ne peuvent pas apparaître partout. »
|
||||
- **Plans :** recherche dans un biome incompatible ; voyage vers le bon climat ; découverte de la variante sauvage.
|
||||
- **Texte / voix :** « Les sources sont distribuées par climat. Ember reste au Nether. Indigo reste dans l'End. »
|
||||
- **Sortie :** `SANCTUARY / CANAPLIA 067`.
|
||||
- **Source :** [découvertes climatiques de l'alpha.208](../../CHANGELOG.md#2620-alpha208).
|
||||
|
||||
## 068 — Une hybridation vérifie réellement ses parents
|
||||
|
||||
- **Format / durée :** expérience, 15 s.
|
||||
- **Accroche :** « Deux plantes côte à côte ne suffisent pas. »
|
||||
- **Plans :** hybridation refusée dans de mauvaises conditions ; correction de l'humidité, de la lumière ou de l'heure ; résultat obtenu.
|
||||
- **Texte / voix :** « Le serveur revalide les parents matures, l'eau, le climat, la lumière et le moment. »
|
||||
- **Sortie :** `SANCTUARY / CANAPLIA 068`.
|
||||
- **Source :** [six hybridations validées côté serveur](../../CHANGELOG.md#2620-alpha208).
|
||||
|
||||
## 069 — Dix têtes deviennent une résine
|
||||
|
||||
- **Format / durée :** transformation, 12 s.
|
||||
- **Accroche :** « Dix têtes identiques. Une seule résine. »
|
||||
- **Plans :** dix objets réunis ; transformation ; application et aura persistante visualisée sobrement.
|
||||
- **Texte / voix :** « L'aura dure 90 secondes et reste attachée à son état serveur. »
|
||||
- **Sortie :** `SANCTUARY / CANAPLIA 069`.
|
||||
- **Source :** [résines Canaplia de l'alpha.208](../../CHANGELOG.md#2620-alpha208).
|
||||
|
||||
## 070 — Un bad trip doit rester accessible
|
||||
|
||||
- **Format / durée :** note de conception, 16 s.
|
||||
- **Accroche :** « Un effet psychédélique ne doit pas rendre le jeu injouable. »
|
||||
- **Plans :** profil normal ; profil bad trip à intensité modérée ; écran des réglages montrant réduction des mouvements, wobble et ghosting.
|
||||
- **Texte / voix :** « Ambiance compose un seul effet à la fois et laisse réduire ses mouvements visuels. »
|
||||
- **Sortie :** `SANCTUARY / ACCESSIBILITÉ 070`.
|
||||
- **Source :** [profils Canaplia et réglages Ambiance](../../CHANGELOG.md#2620-alpha208).
|
||||
|
||||
## 071 — Le Livre interdit est personnel, puis partageable
|
||||
|
||||
- **Format / durée :** interaction, 14 s.
|
||||
- **Accroche :** « Ton livre connaît tes découvertes. Pas celles de tout le serveur. »
|
||||
- **Plans :** deux joueurs avec catalogues différents ; pose sur pupitre ; consultation temporaire par le second joueur ; retrait du livre.
|
||||
- **Texte / voix :** « Sur un pupitre, le savoir se partage. Dans l'inventaire, il redevient personnel. »
|
||||
- **Sortie :** `SANCTUARY / CANAPLIA 071`.
|
||||
- **Source :** [Livre interdit de Canaplia](../../CANAPLIA.md).
|
||||
|
||||
## 072 — Le ciel suit l'heure réelle du serveur
|
||||
|
||||
- **Format / durée :** time-lapse, 15 s.
|
||||
- **Accroche :** « Ici, midi dans le jeu peut être midi dehors. »
|
||||
- **Plans :** horloge système ou carton d'heure sobre ; ciel correspondant ; accéléré contrôlé montrant la continuité du cycle.
|
||||
- **Texte / voix :** « Ambiance peut synchroniser le cycle du ciel sur le fuseau du serveur ou sur un décalage UTC fixe. »
|
||||
- **Sortie :** `SANCTUARY / TEMPS 072`.
|
||||
- **Source :** [runtime Realtime de l'alpha.126](../../CHANGELOG.md#2620-alpha126).
|
||||
|
||||
## 073 — Le calendrier commence le 17 mai 2009
|
||||
|
||||
- **Format / durée :** détail d'auteur, 11 s.
|
||||
- **Accroche :** « Le premier jour de ce monde a une date. »
|
||||
- **Plans :** joueur tenant une horloge ; apparition du HUD calendrier ; gros plan sur la date et le mois.
|
||||
- **Texte / voix :** « L'époque minecraftienne de Sanctuary commence le 17 mai 2009 et possède douze mois. »
|
||||
- **Sortie :** `SANCTUARY / CALENDRIER 073`.
|
||||
- **Source :** [calendrier Realtime](../../CHANGELOG.md#2620-alpha126).
|
||||
|
||||
## 074 — La météo de la journée est déterministe
|
||||
|
||||
- **Format / durée :** une règle du monde, 15 s.
|
||||
- **Accroche :** « La météo n'est choisie qu'une fois par jour. »
|
||||
- **Plans :** matin clair ; transition vers brouillard ou pluie légère ; consultation d'un appareil de prévision.
|
||||
- **Texte / voix :** « Éclaircies, ciel couvert, brouillard, pluie légère ou pluie normale composent le profil quotidien. »
|
||||
- **Sortie :** `SANCTUARY / MÉTÉO 074`.
|
||||
- **Source :** [profils météo de l'alpha.128](../../CHANGELOG.md#2620-alpha128) ; [appareils de prévision de l'alpha.131](../../CHANGELOG.md#2620-alpha131).
|
||||
|
||||
## 075 — Une pleine lune sur quatre devient rouge
|
||||
|
||||
- **Format / durée :** chronique nocturne, 16 s.
|
||||
- **Accroche :** lune blanche pendant deux secondes, puis coupe vers la lune de sang.
|
||||
- **Plans :** pleine lune et nuit blanche ; autre cycle en rouge ; plan du paysage sous la nouvelle ambiance.
|
||||
- **Texte / voix :** « Le cycle vanilla reste sur huit jours. La première nuit de sang arrive au jour 24. »
|
||||
- **Sortie :** `SANCTUARY / LUNE 075`.
|
||||
- **Source :** [cycle lunaire de l'alpha.128](../../CHANGELOG.md#2620-alpha128).
|
||||
|
||||
## 076 — Le premier jour refuse la mauvaise météo
|
||||
|
||||
- **Format / durée :** une règle du monde, 10 s.
|
||||
- **Accroche :** « Le premier jour doit rester lisible. »
|
||||
- **Plans :** première apparition ; ciel clair ; accéléré jusqu'au coucher sans pluie ni brouillard.
|
||||
- **Texte / voix :** « Le jour zéro est toujours clair. L'orage est rare et interdit pendant cette première journée. »
|
||||
- **Sortie :** `SANCTUARY / MÉTÉO 076`.
|
||||
- **Source :** [premier jour de l'alpha.128](../../CHANGELOG.md#2620-alpha128).
|
||||
|
||||
## 077 — Tout bloc lumineux tenu éclaire réellement
|
||||
|
||||
- **Format / durée :** avant / après, 12 s.
|
||||
- **Accroche :** « La torche n'a pas besoin d'être posée. »
|
||||
- **Plans :** grotte noire ; torche prise en main principale ; deuxième lumière tenue en main secondaire avec le terrain et les entités éclairés.
|
||||
- **Texte / voix :** « La lumière locale reprend le niveau vanilla du bloc et décroît d'un niveau par bloc. »
|
||||
- **Sortie :** `SANCTUARY / LUMIÈRE 077`.
|
||||
- **Source :** [Dynamic Torches de l'alpha.130](../../CHANGELOG.md#2620-alpha130).
|
||||
|
||||
## 078 — La pluie ne doit pas simplement assombrir l'image
|
||||
|
||||
- **Format / durée :** avant / après conceptuel, 14 s.
|
||||
- **Accroche :** « Une météo différente n'a pas besoin d'un filtre gris. »
|
||||
- **Plans :** paysage normal ; même cadrage sous pluie légère ; même cadrage sous orage avec seulement la portée du brouillard transformée.
|
||||
- **Texte / voix :** « Les profils météo modifient la visibilité sans écraser la colorimétrie du monde. »
|
||||
- **Sortie :** `SANCTUARY / AMBIANCE 078`.
|
||||
- **Source :** [signature visuelle de l'alpha.129](../../CHANGELOG.md#2620-alpha129).
|
||||
|
||||
## 079 — Un shader remplace proprement l'étalonnage final
|
||||
|
||||
- **Format / durée :** note de laboratoire, 14 s.
|
||||
- **Accroche :** « Deux traitements d'image superposés donnent rarement une meilleure image. »
|
||||
- **Plans :** Ambiance sans shader ; activation d'Iris ; rendu final sans double étalonnage.
|
||||
- **Texte / voix :** « Quand Iris est actif, son traitement final remplace celui d'Ambiance au lieu de s'y empiler. »
|
||||
- **Sortie :** `SANCTUARY / LABO 079`.
|
||||
- **Source :** [compatibilité Iris de l'alpha.129](../../CHANGELOG.md#2620-alpha129).
|
||||
|
||||
## 080 — L'appareil photo capture l'image après le shader
|
||||
|
||||
- **Format / durée :** problème / Sanctuary, 14 s.
|
||||
- **Accroche :** « Une photo doit ressembler à ce que le joueur voit. »
|
||||
- **Plans :** scène avec shader ; visée Canon ; carte photo finale comparée au cadre avant le HUD.
|
||||
- **Texte / voix :** « Canon capture après la composition finale d'Iris et avant l'interface. »
|
||||
- **Sortie :** `SANCTUARY / PHOTO 080`.
|
||||
- **Source :** [capture Canon compatible shaders](../../CHANGELOG.md#2620-alpha129).
|
||||
|
||||
## 081 — Le disque n'était pas trop grave à cause du pitch
|
||||
|
||||
- **Format / durée :** bug / correction, 18 s.
|
||||
- **Accroche :** « Ce disque jouait trop lentement et trop grave. »
|
||||
- **Plans :** extrait de test autorisé du symptôme historique ; carton très bref `stéréo 48 kHz → mono` ; lecture corrigée au même endroit.
|
||||
- **Texte / voix :** « La source stéréo était mal convertie. Sanctuary moyenne maintenant chaque paire gauche-droite, avec une vitesse OpenAL normale. »
|
||||
- **Sortie :** `SANCTUARY / LABO 081`.
|
||||
- **Source :** [correction audio de l'alpha.221](../../CHANGELOG.md#2620-alpha221).
|
||||
|
||||
## 082 — Une demi-dalle peut ouvrir 128 conteneurs
|
||||
|
||||
- **Format / durée :** révélation fonctionnelle, 15 s.
|
||||
- **Accroche :** « Ce terminal ne contient aucun objet. »
|
||||
- **Plans :** terminal fixé sur un coffre ; recul révélant le réseau contigu ; ouverture de la grille agrégée.
|
||||
- **Texte / voix :** « Il donne accès à un réseau borné de 128 coffres, coffres piégés ou barils chargés. »
|
||||
- **Sortie :** `SANCTUARY / STOCKAGE 082`.
|
||||
- **Source :** [terminal Red-Stoner de l'alpha.218](../../CHANGELOG.md#2620-alpha218).
|
||||
|
||||
## 083 — Le terminal affiche 54 types par page
|
||||
|
||||
- **Format / durée :** interaction, 12 s.
|
||||
- **Accroche :** « Une page. Cinquante-quatre types. »
|
||||
- **Plans :** première page ; défilement ; changement de page en conservant l'ordre des emplacements.
|
||||
- **Texte / voix :** « La grille agrège les types d'objets, pas les cases physiques des coffres. »
|
||||
- **Sortie :** `SANCTUARY / STOCKAGE 083`.
|
||||
- **Source :** [pagination du terminal, alpha.218](../../CHANGELOG.md#2620-alpha218).
|
||||
|
||||
## 084 — La recherche du terminal comprend Unicode
|
||||
|
||||
- **Format / durée :** démonstration d'interface, 13 s.
|
||||
- **Accroche :** « Accents et alphabets Unicode restent recherchables. »
|
||||
- **Plans :** saisie d'un nom Unicode réellement présent dans le catalogue ; filtrage instantané ; retrait d'une pile sans déplacement des résultats restants.
|
||||
- **Texte / voix :** « Les quantités deviennent compactes et les emplacements ne glissent plus lorsqu'une pile est vidée. »
|
||||
- **Sortie :** `SANCTUARY / STOCKAGE 084`.
|
||||
- **Source :** [recherche et stabilité du terminal, alpha.222](../../CHANGELOG.md#2620-alpha222).
|
||||
|
||||
## 085 — Le terminal se comporte comme un vrai conteneur
|
||||
|
||||
- **Format / durée :** problème / Sanctuary, 15 s.
|
||||
- **Accroche :** « Pas de bouton propriétaire pour fermer. Pas de clics réinventés. »
|
||||
- **Plans :** clic gauche ; clic droit ; shift-clic depuis une sixième rangée d'inventaire ; fermeture vanilla.
|
||||
- **Texte / voix :** « Les gestes de Minecraft fonctionnent sur toutes les rangées débloquées. Le serveur décide où déposer chaque pile. »
|
||||
- **Sortie :** `SANCTUARY / STOCKAGE 085`.
|
||||
- **Source :** [menu de conteneur du terminal, alpha.220](../../CHANGELOG.md#2620-alpha220).
|
||||
|
||||
## 086 — Un clic molette trie sans toucher à la hotbar
|
||||
|
||||
- **Format / durée :** interaction cachée, 10 s.
|
||||
- **Accroche :** « Ouvre un coffre. Clique sur la molette. »
|
||||
- **Plans :** coffre volontairement désordonné ; clic molette ; rangement immédiat, puis gros plan prouvant que la hotbar n'a pas bougé.
|
||||
- **Texte / voix :** « Coffres, barils, Shulkers, entonnoirs, distributeurs et inventaire joueur utilisent la même règle. »
|
||||
- **Sortie :** `SANCTUARY / RANGEMENT 086`.
|
||||
- **Source :** [tri natif de l'alpha.224](../../CHANGELOG.md#2620-alpha224).
|
||||
|
||||
## 087 — Un ordinateur Minecraft avec six ports redstone
|
||||
|
||||
- **Format / durée :** démonstration technique, 17 s.
|
||||
- **Accroche :** « Ce bloc exécute réellement un programme assembleur. »
|
||||
- **Plans :** saisie d'un programme court ; impulsion sur un port directionnel ; résultat sur lampe et comparateur.
|
||||
- **Texte / voix :** « Programme borné, RAM, inventaire, six ports, impulsions et sortie comparateur : la machine virtuelle historique reste jouable. »
|
||||
- **Sortie :** `SANCTUARY / RED-STONER 087`.
|
||||
- **Source :** [ordinateur 8 bits](../../README.md#red-stoner-et-photographie).
|
||||
|
||||
## 088 — Seize pellicules, une photo sur carte
|
||||
|
||||
- **Format / durée :** collection, 16 s.
|
||||
- **Accroche :** « L'appareil ne crée pas un fichier hors du jeu. »
|
||||
- **Plans :** choix de trois pellicules contrastées parmi les seize ; visée ; trois cartes photo exposées côte à côte.
|
||||
- **Texte / voix :** « Canon transforme la scène en objet Minecraft que l'on peut conserver et montrer. »
|
||||
- **Sortie :** `SANCTUARY / CANON 088`.
|
||||
- **Source :** [Canon et styles de pellicule](../../README.md#red-stoner-et-photographie).
|
||||
|
||||
## 089 — Un coffre-fort est une coque creuse de 26 blocs
|
||||
|
||||
- **Format / durée :** construction, 15 s.
|
||||
- **Accroche :** « Ni un mur. Ni un cube plein. »
|
||||
- **Plans :** structure incorrecte refusée ; coque 3 × 3 × 3 avec centre libre ; activation des textures sur les six faces.
|
||||
- **Texte / voix :** « Les 26 Safe Blocks doivent appartenir au même joueur. Le centre peut cacher n'importe quel bloc. »
|
||||
- **Sortie :** `SANCTUARY / COFFRE-FORT 089`.
|
||||
- **Source :** [coque du coffre-fort, alpha.205](../../CHANGELOG.md#2620-alpha205).
|
||||
|
||||
## 090 — Pour braquer un coffre, il faut déjà en posséder un
|
||||
|
||||
- **Format / durée :** règle de braquage, 18 s.
|
||||
- **Accroche :** « Un voleur doit mettre sa propre banque en jeu. »
|
||||
- **Plans :** forage refusé sans coffre personnel ; construction ou preuve du coffre ; forage continu de 60 secondes et transfert final.
|
||||
- **Texte / voix :** « Le propriétaire doit être connecté. Une réussite vole 20 %, consomme la foreuse, alerte la victime et protège le coffre pendant 24 heures. »
|
||||
- **Sortie :** `SANCTUARY / BRAQUAGE 090`.
|
||||
- **Source :** [forage et transfert de l'alpha.202](../../CHANGELOG.md#2620-alpha202) ; [condition de propriété de l'alpha.225](../../CHANGELOG.md#2620-alpha225).
|
||||
|
||||
## 091 — Le Creeperlock explose sans casser le monde
|
||||
|
||||
- **Format / durée :** arsenal, 12 s.
|
||||
- **Accroche :** « Une explosion de combat, pas une explosion de terrain. »
|
||||
- **Plans :** tir sur plusieurs cibles ; souffle et recul ; même décor intact après l'impact.
|
||||
- **Texte / voix :** « Le Creeperlock consomme une poudre à canon et inflige des dégâts selon la distance, sans détruire de bloc. »
|
||||
- **Sortie :** `SANCTUARY / OUCH 091`.
|
||||
- **Source :** [Creeperlock](../../ouch/README.md#arsenal).
|
||||
|
||||
## 092 — La Mitraillette tire les métaux de ton inventaire
|
||||
|
||||
- **Format / durée :** arsenal, 14 s.
|
||||
- **Accroche :** « La munition décide des dégâts. »
|
||||
- **Plans :** pépites cuivre, fer, or, argent et titane alignées ; tir continu ; pépite tenue en main secondaire choisie en priorité.
|
||||
- **Texte / voix :** « Cinq métaux, cinq puissances. Un tir toutes les cinq ticks jusqu'à 18 blocs. »
|
||||
- **Sortie :** `SANCTUARY / OUCH 092`.
|
||||
- **Source :** [Mitraillette](../../ouch/README.md#arsenal).
|
||||
|
||||
## 093 — L'Expulseur transforme ton inventaire en projectiles
|
||||
|
||||
- **Format / durée :** arsenal absurde, 13 s.
|
||||
- **Accroche :** « Ce qui se trouve dans ton sac peut devenir une arme. »
|
||||
- **Plans :** inventaire principal contenant les munitions improvisées ; jusqu'à huit objets projetés ; hotbar montrée intacte.
|
||||
- **Texte / voix :** « Les objets restent physiques et récupérables. Les neuf cases de la barre d'action ne sont jamais consommées. »
|
||||
- **Sortie :** `SANCTUARY / OUCH 093`.
|
||||
- **Source :** [Expulseur](../../ouch/README.md#arsenal).
|
||||
|
||||
## 094 — L'Aspirateur retire aussi le bloc visé
|
||||
|
||||
- **Format / durée :** arsenal utilitaire, 13 s.
|
||||
- **Accroche :** « Attirer les objets n'était que la moitié de la fonction. »
|
||||
- **Plans :** objets au sol attirés dans un rayon de dix blocs ; visée d'un bloc cassable ; extraction et récupération.
|
||||
- **Texte / voix :** « Il refuse les fluides, les block entities et toute interaction sans permission. »
|
||||
- **Sortie :** `SANCTUARY / OUCH 094`.
|
||||
- **Source :** [Aspirateur](../../ouch/README.md#arsenal).
|
||||
|
||||
## 095 — Chaque minerai brut change le Mining Rifle
|
||||
|
||||
- **Format / durée :** arsenal, 17 s.
|
||||
- **Accroche :** « Cuivre, fer, or, argent ou titane : le fusil ne tire pas la même chose. »
|
||||
- **Plans :** cinq munitions ; rechargement plus rapide avec l'or ; bonus argent sur un mort-vivant ; titane traversant une seconde cible.
|
||||
- **Texte / voix :** « L'argent ajoute des dégâts aux morts-vivants. Le titane conserve 75 % des dégâts sur une seconde cible. »
|
||||
- **Sortie :** `SANCTUARY / OUCH 095`.
|
||||
- **Source :** [Mining Rifle](../../ouch/README.md#arsenal).
|
||||
|
||||
## 096 — L'Arc bêta tire quatre flèches par seconde
|
||||
|
||||
- **Format / durée :** arsenal historique, 13 s.
|
||||
- **Accroche :** « Avant l'animation de charge, l'arc tirait immédiatement. »
|
||||
- **Plans :** premier clic instantané ; maintien produisant quatre tirs par seconde ; relâchement sans projectile supplémentaire.
|
||||
- **Texte / voix :** « Chaque tir consomme une vraie flèche vanilla et conserve ses effets compatibles. »
|
||||
- **Sortie :** `SANCTUARY / OUCH 096`.
|
||||
- **Source :** [Arc bêta](../../ouch/README.md#arsenal).
|
||||
|
||||
## 097 — Le Shop commence avec trois offres
|
||||
|
||||
- **Format / durée :** progression économique, 14 s.
|
||||
- **Accroche :** « Le magasin n'est pas entièrement ouvert au début. »
|
||||
- **Plans :** trois cartes actives et six verrouillées ; achat d'un emplacement ; grille progressivement étendue jusqu'à neuf offres.
|
||||
- **Texte / voix :** « Les six emplacements supplémentaires coûtent 2, 4, 8, 12, 18 puis 25 niveaux. »
|
||||
- **Sortie :** `SANCTUARY / SHOP 097`.
|
||||
- **Source :** [progression du Shop, alpha.193](../../CHANGELOG.md#2620-alpha193).
|
||||
|
||||
## 098 — Le marché noir est construit avec de vrais panneaux
|
||||
|
||||
- **Format / durée :** interaction sociale, 18 s.
|
||||
- **Accroche :** « Ici, une annonce existe parce qu'un panneau existe dans le monde. »
|
||||
- **Plans :** vendeur tenant un objet et cliquant sur un panneau ; prix et pseudonyme visibles ; achat par un second joueur puis livraison Mailbox.
|
||||
- **Texte / voix :** « L'objet, le paiement et les composants sont séquestrés et livrés par le serveur. Casser le panneau obéit à son propriétaire. »
|
||||
- **Sortie :** `SANCTUARY / MARCHÉ 098`.
|
||||
- **Source :** [Black Market physique](../../sanctuary/BLACK_MARKET.md#utilisation).
|
||||
|
||||
## 099 — Les joueurs peuvent écrire leurs propres quêtes
|
||||
|
||||
- **Format / durée :** interaction sociale, 18 s.
|
||||
- **Accroche :** « KILL. BRING. PAY. Une quête écrite par un autre joueur. »
|
||||
- **Plans :** création sur le panneau rose ; lecture et confirmation par un second joueur ; complétion puis arrivée de la récompense dans la Mailbox.
|
||||
- **Texte / voix :** « La récompense est déposée dès la publication. Le serveur suit l'objectif et ne la livre qu'une fois. »
|
||||
- **Sortie :** `SANCTUARY / QUÊTES 099`.
|
||||
- **Source :** [demandes communautaires, alpha.201](../../CHANGELOG.md#2620-alpha201).
|
||||
|
||||
## 100 — Appuie sur J
|
||||
|
||||
- **Format / durée :** absurdité sèche, 9 s.
|
||||
- **Accroche :** plan parfaitement sérieux du joueur, immobile au centre d'une pièce blanche.
|
||||
- **Plans :** pression sur `J` ; action de six secondes ; boule créée, puis transformation de neuf boules en cube dans une coupe séparée.
|
||||
- **Texte / voix :** « Sanctuary réinterprète aussi les mauvaises idées. »
|
||||
- **Sortie :** `SANCTUARY / ONLY FUN 100`.
|
||||
- **Source :** [action et cube](../../onlyfun/ABJECTION.md#abj-01--actions-serveur) ; [raccourci configurable](../../onlyfun/ABJECTION.md#abj-03--raccourci-configurable).
|
||||
|
||||
## Ordre de production recommandé
|
||||
|
||||
Ne pas tourner les vidéos dans leur ordre de publication. Les regrouper par décor réduit fortement le temps de capture :
|
||||
|
||||
1. **Laboratoire interface :** 003 à 019, 083 à 086 et 097.
|
||||
2. **Archipel et worldgen :** 002, 020 à 027, 053 et 072 à 079.
|
||||
3. **Alpha / maison Indev :** 028 à 031.
|
||||
4. **Terrain de déplacement :** 032 à 039.
|
||||
5. **Bestiaire contrôlé :** 040 à 059.
|
||||
6. **Ferme et cuisine :** 060 à 071.
|
||||
7. **Atelier Red-Stoner / Another World :** 080 à 090.
|
||||
8. **Stand de tir Ouch :** 091 à 096.
|
||||
9. **Place de serveur :** 098 à 100.
|
||||
|
||||
## Premier lot public conseillé
|
||||
|
||||
Avant de produire les cent vidéos, valider la grammaire avec douze épisodes très différents : **001, 003, 006, 014, 020, 031, 041, 053, 062, 077, 086 et 099**. Ils couvrent le manifeste, l'interface, une mécanique spectaculaire, l'exploration, une créature, l'agriculture, l'ambiance et le multijoueur sans nécessiter cent tournages pour apprendre ce qui fonctionne.
|
||||
+2
-2
@@ -16,7 +16,7 @@ ambiance_version=0.0.0-alpha.22
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.13
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.56
|
||||
anotherworld_version=0.0.0-alpha.57
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.38
|
||||
itsalive_lifecycle=active
|
||||
@@ -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.130
|
||||
sanctuary_version=0.0.0-alpha.131
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.226
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "pending_release",
|
||||
"module": "sanctuary",
|
||||
"source_module_version": "0.0.0-alpha.130",
|
||||
"target_module_version": "0.0.0-alpha.131",
|
||||
"integration_modules": {
|
||||
"anotherworld": {
|
||||
"source": "0.0.0-alpha.56",
|
||||
"target": "0.0.0-alpha.57",
|
||||
"data_version_changed": false,
|
||||
"network_protocol_changed": false
|
||||
}
|
||||
},
|
||||
"source_data_version": 13,
|
||||
"target_data_version": 14,
|
||||
"migration": {
|
||||
"kind": "monotone_addition",
|
||||
"save_key": "sanctuary:backroom",
|
||||
"existing_player_progress_preserved": true,
|
||||
"existing_shop_data_preserved": true,
|
||||
"existing_mailboxes_preserved": true,
|
||||
"mailbox_transaction_history_default": [],
|
||||
"backroom_loss_registry_default": [],
|
||||
"backroom_chest_registry_default": []
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"network_protocol_changed": false,
|
||||
"registry_ids_added": [
|
||||
"sanctuary:backroom",
|
||||
"sanctuary:backroom_loss_id",
|
||||
"sanctuary:backroom_architecture"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Backroom
|
||||
|
||||
La Backroom est une dimension persistante de récupération et de construction. Elle n’est jamais
|
||||
réinitialisée : routes, bases, coffres et bâtiments posés par les habitants restent dans le monde.
|
||||
Sa zone architecturale forme un disque de 2 000 blocs autour de l’origine.
|
||||
|
||||
## Entrer et sortir
|
||||
|
||||
- S’accroupir et utiliser un lit hors de la Backroom déclenche l’entrée, sans remplacer l’usage
|
||||
normal du lit.
|
||||
- Avant toute téléportation, le serveur transfère atomiquement vers la Mailbox enregistrée les
|
||||
43 emplacements vanilla, les 18 emplacements étendus, l’œuf compagnon et la cape équipée.
|
||||
- Si aucune Mailbox extérieure n’est chargée, ou si elle ne peut pas recevoir l’intégralité du
|
||||
colis, rien ne bouge et l’entrée est refusée. Une Mailbox construite dans la Backroom n’est jamais
|
||||
une destination d’entrée et ne permet donc pas d’importer du matériel.
|
||||
- L’identifiant de transaction est conservé par la Mailbox. Après une interruption, la connexion
|
||||
suivante termine exactement une fois le vidage correspondant ou annule une transaction qui n’a
|
||||
jamais atteint la Mailbox.
|
||||
- Dans la Backroom, tout lit — généré ou posé par un joueur — ramène au lit de réapparition normal,
|
||||
ou au point d’apparition global s’il n’existe plus. Le joueur conserve alors ses trouvailles.
|
||||
- Aucun autre usage de lit ne définit un point de réapparition dans la dimension.
|
||||
- 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.
|
||||
|
||||
## Objets perdus et coffres
|
||||
|
||||
Les piles qui disparaissent naturellement ou tombent dans le vide rejoignent un registre mondial
|
||||
borné. Une mort de joueur dans la Backroom y replace aussi tout ce qu’il transportait. Chaque pile
|
||||
réelle matérialisée reçoit un identifiant persistant : elle peut passer du registre à un coffre, puis
|
||||
à un joueur et revenir au registre, mais elle ne peut jamais exister deux fois.
|
||||
Une perte associée à un joueur lui reste réservée pendant 24 000 ticks avant de pouvoir rejoindre
|
||||
les tirages historiques partagés ; une perte sans propriétaire est immédiatement partagée.
|
||||
|
||||
Tous les coffres vanilla de la Backroom, générés ou posés, deviennent des nœuds de butin renouvelés
|
||||
toutes les 12 000 ticks lors de leur prochaine ouverture. Le renouvellement rend au registre les
|
||||
objets perdus non pris, efface le contenu ordinaire et produit le plus souvent du vide ou des
|
||||
ressources pauvres : terre, pierre, planches, torches, nourriture et outils simples. Le butin ancien
|
||||
partagé et les objets de valeur restent rares. Reposer un coffre au même emplacement pendant le
|
||||
même cycle ne crée pas un nouveau tirage ; multiplier les emplacements donne en revanche bien des
|
||||
tirages indépendants, au prix des ressources trouvées ou produites sur place.
|
||||
|
||||
Pendant une expédition, chaque coffre distinct est un essai personnel. Le premier a 75 % de chance
|
||||
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
|
||||
|
||||
Les plans sont déterministes par grandes zones et utilisent cinq familles composées :
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
## Exploitation
|
||||
|
||||
Les données `sanctuary:backroom` utilisent leur propre `data_version`, ainsi que des limites pour les
|
||||
transactions, pertes, coffres suivis, visites et arrivées récentes. Les opérateurs disposent de
|
||||
`/backroom status` et `/backroom inspect <joueur>` pour contrôler les registres sans les modifier.
|
||||
+90
-16
@@ -156,6 +156,14 @@ tasks.register("notchVictoryStateSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.data.NotchVictoryStateSmoke"
|
||||
}
|
||||
|
||||
tasks.register("backroomModelSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks bounded Backroom entry data, personal loot priority and composed architecture zones."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomModelSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyBlackMarket") {
|
||||
group = "verification"
|
||||
description = "Checks the physical sign market, flash Shop, escrow ledger and mailbox delivery."
|
||||
@@ -191,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| !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")
|
||||
@@ -1200,13 +1208,13 @@ tasks.register("verifySanctuary") {
|
||||
}
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 13
|
||||
if (manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifest.depends?.jei != ">=30.24.0.176"
|
||||
|| manifest.depends?."fabric-api" != ">=0.155.0+26.2"
|
||||
|| !manifest.entrypoints?.client?.contains("fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient")
|
||||
|| !manifest.mixins?.contains("sanctuary.mixins.json")) {
|
||||
throw new GradleException("Sanctuary must expose data v13, protocol 24, JEI and the client entrypoint")
|
||||
throw new GradleException("Sanctuary must expose data v14, protocol 24, JEI and the client entrypoint")
|
||||
}
|
||||
def capeMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.155-cape-equipment-to-alpha.156.json"))
|
||||
@@ -1691,8 +1699,8 @@ tasks.register("verifySanctuary") {
|
||||
|
||||
def worldgenRoot = file("src/main/resources/data/sanctuary/worldgen")
|
||||
def worldgenCounts = [
|
||||
density_function: 12, biome: 6, configured_feature: 13, configured_carver: 1,
|
||||
placed_feature: 19, noise_settings: 3, world_preset: 2
|
||||
density_function: 12, biome: 7, configured_feature: 14, configured_carver: 1,
|
||||
placed_feature: 20, noise_settings: 4, world_preset: 2
|
||||
]
|
||||
worldgenCounts.each { type, expected ->
|
||||
def actual = fileTree(new File(worldgenRoot, type.toString())).matching { include "**/*.json" }.files.size()
|
||||
@@ -2907,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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
|
||||
@@ -3073,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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
|
||||
@@ -3111,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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]
|
||||
@@ -3142,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.195"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|
||||
|| migration.compatibility?.sanctuary_data_version != 11
|
||||
@@ -3173,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|
||||
|| migration.mailbox?.id != "sanctuary:delivery_box"
|
||||
|| migration.mailbox?.horizontal_facing != true
|
||||
@@ -3201,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| !transfer.contains("shouldAppendOverflow")
|
||||
|| !transfer.contains("isVanillaStorageRange")
|
||||
|| !transfer.contains("hotbarStart")
|
||||
@@ -3247,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|
||||
|| migration.quest_requests?.save_key != "quest_requests"
|
||||
|| migration.quest_requests?.data_version != 1
|
||||
@@ -3305,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -3374,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -3453,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.130"
|
||||
|| migration.white_cape?.crafting_material_count != 6
|
||||
|| migration.white_cape?.maximum_pattern_layers != 6
|
||||
@@ -3508,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|
||||
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|
||||
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
|
||||
@@ -3530,7 +3538,73 @@ tasks.register("verifyAlphaIndev") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyBackroom") {
|
||||
group = "verification"
|
||||
description = "Checks Backroom dimension, migration, Mailbox boundary, loss registry and translations."
|
||||
inputs.file(file("BACKROOM.md"))
|
||||
inputs.file(rootProject.file("pack/migrations/sanctuary-backroom-data-v14.json"))
|
||||
inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom"))
|
||||
inputs.files(fileTree("src/main/resources/data/sanctuary") { include "**/*backroom*.json" })
|
||||
inputs.files(fileTree("src/main/resources/assets/sanctuary/lang") { include "*.json" })
|
||||
inputs.files(
|
||||
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/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 presets = [
|
||||
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")
|
||||
].collect { new JsonSlurper().parse(it) }
|
||||
def data = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomData.java").text
|
||||
def inventory = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomInventory.java").text
|
||||
def service = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomService.java").text
|
||||
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 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"
|
||||
|| 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"
|
||||
|| migration.compatibility?.network_protocol_changed != false
|
||||
|| 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"
|
||||
|| !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")
|
||||
|| !inventory.contains("companionEgg") || !inventory.contains("equippedCape")
|
||||
|| !service.contains("ARRIVAL_RADIUS = 2_000") || !service.contains("insertTransaction")
|
||||
|| !service.contains("AUTHORIZED_TRANSITIONS")
|
||||
|| !service.contains("level -> !isBackroom(level)")
|
||||
|| !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()")
|
||||
|| !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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("backroomModelSmoke")
|
||||
dependsOn tasks.named("verifyBackroom")
|
||||
dependsOn tasks.named("notchVictoryStateSmoke")
|
||||
dependsOn tasks.named("verifySanctuary")
|
||||
dependsOn tasks.named("verifyDeliveryBoxOrientation")
|
||||
|
||||
@@ -26,6 +26,7 @@ import fr.koka99cab.sanctuary26.sanctuary.shop.ShopService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.BlackMarketService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestBoardService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -34,7 +35,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class SanctuaryMod implements ModInitializer {
|
||||
public static final String MOD_ID = "sanctuary";
|
||||
public static final String DISPLAY_NAME = "Sanctuary";
|
||||
public static final int CURRENT_DATA_VERSION = 13;
|
||||
public static final int CURRENT_DATA_VERSION = 14;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public SanctuaryMod() {
|
||||
@@ -53,6 +54,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
SanctuarySpawn.register();
|
||||
SanctuaryWorldProfileData.register();
|
||||
SanctuaryRegistries.initialize();
|
||||
BackroomService.initialize();
|
||||
SanctuaryBulkActions.initialize();
|
||||
SanctuaryInventoryCapacity.initialize();
|
||||
SanctuaryHotbarRows.initialize();
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.backroom;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Public, one-way integration point for rare materials owned by other modules. */
|
||||
public final class BackroomMaterialApi {
|
||||
private static Block anomalyMaterial = Blocks.WARPED_WART_BLOCK;
|
||||
|
||||
private BackroomMaterialApi() {}
|
||||
|
||||
public static void registerAnomalyMaterial(Block material) {
|
||||
anomalyMaterial = Objects.requireNonNull(material, "Backroom anomaly material");
|
||||
}
|
||||
|
||||
public static Block anomalyMaterial() {
|
||||
return anomalyMaterial;
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.ChestType;
|
||||
|
||||
/** Renewable loot nodes backed by the non-duplicating real-item registry. */
|
||||
public final class BackroomChestService {
|
||||
public static final long REFRESH_TICKS = 12_000L;
|
||||
|
||||
private BackroomChestService() {}
|
||||
|
||||
public static void open(ChestBlockEntity opened, ServerPlayer player) {
|
||||
if (!(opened.getLevel() instanceof ServerLevel level) || !BackroomService.isBackroom(level)) return;
|
||||
ChestGroup group = group(level, opened);
|
||||
BackroomData data = BackroomData.get(level.getServer());
|
||||
long cycle = Math.floorDiv(level.getGameTime(), REFRESH_TICKS);
|
||||
BackroomData.ChestState state = data.chest(group.anchor).orElse(null);
|
||||
ArrayList<UUID> assigned = state == null ? new ArrayList<>() : new ArrayList<>(state.assignedIds());
|
||||
boolean refreshed = state == null || state.cycle() != cycle;
|
||||
|
||||
if (refreshed) {
|
||||
data.reconcileChest(group.anchor, markers(group));
|
||||
group.clear();
|
||||
assigned.clear();
|
||||
}
|
||||
|
||||
BackroomData.ChestVisit visit = data.visitChest(player.getUUID(), group.anchor);
|
||||
boolean personalInserted = false;
|
||||
if (visit.firstVisit() && personalRoll(player.getRandom(), visit.ordinal())) {
|
||||
BackroomData.MaterializedLoss loss = data.takePersonal(player.getUUID(), group.anchor).orElse(null);
|
||||
if (loss != null && group.insertLoss(loss.stack())) {
|
||||
assigned.add(loss.id());
|
||||
personalInserted = true;
|
||||
} else if (loss != null) {
|
||||
data.capture(loss.stack(), player.getUUID(), "chest_full", level.getGameTime());
|
||||
}
|
||||
}
|
||||
if (refreshed) rollGeneralLoot(level, group, data, assigned, cycle);
|
||||
if (refreshed || personalInserted) data.updateChest(group.anchor, cycle, assigned);
|
||||
}
|
||||
|
||||
public static boolean personalRoll(RandomSource random, int ordinal) {
|
||||
if (ordinal <= 0) return false;
|
||||
if (ordinal == 1) return random.nextFloat() < 0.75F;
|
||||
if (ordinal == 2) return random.nextFloat() < 0.90F;
|
||||
if (ordinal == 3) return true;
|
||||
return random.nextFloat() < 0.65F;
|
||||
}
|
||||
|
||||
private static void rollGeneralLoot(ServerLevel level, ChestGroup group, BackroomData data,
|
||||
List<UUID> assigned, long cycle) {
|
||||
RandomSource random = RandomSource.create(level.getSeed() ^ group.anchor ^ cycle * 0x9E3779B97F4A7C15L);
|
||||
int roll = random.nextInt(100);
|
||||
if (roll < 55) return;
|
||||
if (roll < 74) {
|
||||
insert(group, random, Items.DIRT, 4, 20);
|
||||
if (random.nextBoolean()) insert(group, random, Items.OAK_PLANKS, 2, 10);
|
||||
return;
|
||||
}
|
||||
if (roll < 90) {
|
||||
insert(group, random, Items.COBBLESTONE, 6, 24);
|
||||
insert(group, random, random.nextBoolean() ? Items.TORCH : Items.BREAD, 2, 7);
|
||||
if (random.nextInt(3) == 0) group.insert(new ItemStack(Items.WOODEN_PICKAXE));
|
||||
return;
|
||||
}
|
||||
if (roll < 97) {
|
||||
data.takeShared(group.anchor, level.getGameTime()).ifPresent(loss -> {
|
||||
if (group.insertLoss(loss.stack())) assigned.add(loss.id());
|
||||
else data.capture(loss.stack(), null, "chest_full", level.getGameTime());
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (roll < 99) {
|
||||
group.insert(new ItemStack(Items.IRON_PICKAXE));
|
||||
insert(group, random, Items.ENDER_PEARL, 1, 2);
|
||||
} else {
|
||||
group.insert(new ItemStack(Items.DIAMOND));
|
||||
}
|
||||
}
|
||||
|
||||
private static void insert(ChestGroup group, RandomSource random, Item item, int minimum, int maximum) {
|
||||
group.insert(new ItemStack(item, minimum + random.nextInt(maximum - minimum + 1)));
|
||||
}
|
||||
|
||||
private static List<UUID> markers(ChestGroup group) {
|
||||
ArrayList<UUID> result = new ArrayList<>();
|
||||
for (ChestBlockEntity chest : group.chests) for (int slot = 0; slot < chest.getContainerSize(); slot++) {
|
||||
UUID marker = chest.getItem(slot).get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (marker != null) result.add(marker);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ChestGroup group(ServerLevel level, ChestBlockEntity opened) {
|
||||
BlockPos pos = opened.getBlockPos();
|
||||
BlockState state = opened.getBlockState();
|
||||
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
|
||||
return new ChestGroup(pos.asLong(), List.of(opened));
|
||||
}
|
||||
BlockPos connectedPos = ChestBlock.getConnectedBlockPos(pos, state);
|
||||
if (!(level.getBlockEntity(connectedPos) instanceof ChestBlockEntity connected)) {
|
||||
return new ChestGroup(pos.asLong(), List.of(opened));
|
||||
}
|
||||
long anchor = Math.min(pos.asLong(), connectedPos.asLong());
|
||||
return pos.asLong() == anchor
|
||||
? new ChestGroup(anchor, List.of(opened, connected))
|
||||
: new ChestGroup(anchor, List.of(connected, opened));
|
||||
}
|
||||
|
||||
private record ChestGroup(long anchor, List<ChestBlockEntity> chests) {
|
||||
private void clear() {
|
||||
chests.forEach(chest -> { chest.clearContent(); chest.setChanged(); });
|
||||
}
|
||||
|
||||
private boolean insert(ItemStack offered) {
|
||||
if (offered == null || offered.isEmpty()) return false;
|
||||
for (ChestBlockEntity chest : chests) if (insertInto(chest, offered)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Keeps every real loss in the physical anchor chest tracked by the persisted state. */
|
||||
private boolean insertLoss(ItemStack offered) {
|
||||
return offered != null && !offered.isEmpty() && insertInto(chests.getFirst(), offered);
|
||||
}
|
||||
|
||||
private static boolean insertInto(ChestBlockEntity chest, ItemStack offered) {
|
||||
for (int slot = 0; slot < chest.getContainerSize(); slot++) {
|
||||
if (!chest.getItem(slot).isEmpty()) continue;
|
||||
chest.setItem(slot, offered.copy());
|
||||
chest.setChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Persistent, bounded state for Backroom entries and the real-item loss pool. */
|
||||
public final class BackroomData extends SavedData {
|
||||
public static final String SAVE_KEY = "backroom";
|
||||
public static final int DATA_VERSION = 1;
|
||||
public static final int MAX_PENDING_ENTRIES = 256;
|
||||
public static final int MAX_ENTRY_SLOTS = 64;
|
||||
public static final int MAX_LOST_ITEMS = 16_384;
|
||||
public static final int MAX_CHEST_STATES = 65_536;
|
||||
public static final int MAX_PLAYER_STATES = 4_096;
|
||||
public static final int MAX_VISITED_CHESTS = 128;
|
||||
public static final int MAX_RECENT_ARRIVALS = 8;
|
||||
public static final long PERSONAL_RESERVATION_TICKS = 24_000L;
|
||||
public static final String LOST_POOL = "pool";
|
||||
public static final String LOST_CHEST = "chest";
|
||||
|
||||
public static final Codec<EntrySlot> ENTRY_SLOT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("location").forGetter(EntrySlot::location),
|
||||
Codec.INT.fieldOf("index").forGetter(EntrySlot::index),
|
||||
ItemStack.CODEC.fieldOf("item").forGetter(EntrySlot::item)
|
||||
).apply(instance, EntrySlot::new));
|
||||
private static final Codec<PendingEntry> PENDING_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("player").forGetter(PendingEntry::player),
|
||||
Codec.STRING.fieldOf("transaction").forGetter(PendingEntry::transaction),
|
||||
ENTRY_SLOT_CODEC.listOf(0, MAX_ENTRY_SLOTS).fieldOf("slots").forGetter(PendingEntry::slots)
|
||||
).apply(instance, PendingEntry::new));
|
||||
private static final Codec<LostItem> LOST_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("id").forGetter(LostItem::id),
|
||||
Codec.STRING.optionalFieldOf("owner", "").forGetter(LostItem::owner),
|
||||
Codec.STRING.fieldOf("cause").forGetter(LostItem::cause),
|
||||
Codec.LONG.fieldOf("lost_at").forGetter(LostItem::lostAt),
|
||||
ItemStack.CODEC.fieldOf("item").forGetter(LostItem::item),
|
||||
Codec.STRING.optionalFieldOf("state", LOST_POOL).forGetter(LostItem::state),
|
||||
Codec.LONG.optionalFieldOf("chest", Long.MIN_VALUE).forGetter(LostItem::chest)
|
||||
).apply(instance, LostItem::new));
|
||||
private static final Codec<ChestState> CHEST_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.LONG.fieldOf("pos").forGetter(ChestState::pos),
|
||||
Codec.LONG.fieldOf("cycle").forGetter(ChestState::cycle),
|
||||
Codec.STRING.listOf(0, 54).optionalFieldOf("assigned", List.of()).forGetter(ChestState::assigned)
|
||||
).apply(instance, ChestState::new));
|
||||
private static final Codec<PlayerState> PLAYER_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("player").forGetter(PlayerState::player),
|
||||
Codec.INT.optionalFieldOf("personal_attempts", 0).forGetter(PlayerState::personalAttempts),
|
||||
Codec.LONG.listOf(0, MAX_VISITED_CHESTS).optionalFieldOf("visited_chests", List.of()).forGetter(PlayerState::visitedChests),
|
||||
Codec.LONG.listOf(0, MAX_RECENT_ARRIVALS).optionalFieldOf("recent_arrivals", List.of()).forGetter(PlayerState::recentArrivals)
|
||||
).apply(instance, PlayerState::new));
|
||||
private static final Codec<BackroomData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
|
||||
PENDING_CODEC.listOf(0, MAX_PENDING_ENTRIES).optionalFieldOf("pending_entries", List.of()).forGetter(data -> data.pendingEntries),
|
||||
LOST_CODEC.listOf(0, MAX_LOST_ITEMS).optionalFieldOf("lost_items", List.of()).forGetter(data -> data.lostItems),
|
||||
CHEST_CODEC.listOf(0, MAX_CHEST_STATES).optionalFieldOf("chests", List.of()).forGetter(data -> data.chests),
|
||||
PLAYER_CODEC.listOf(0, MAX_PLAYER_STATES).optionalFieldOf("players", List.of()).forGetter(data -> data.players)
|
||||
).apply(instance, BackroomData::new));
|
||||
private static final SavedDataType<BackroomData> TYPE = new SavedDataType<>(
|
||||
SanctuaryMod.id(SAVE_KEY), BackroomData::new, CODEC, null);
|
||||
|
||||
private int dataVersion;
|
||||
private final List<PendingEntry> pendingEntries;
|
||||
private final List<LostItem> lostItems;
|
||||
private final List<ChestState> chests;
|
||||
private final Map<Long, Integer> chestIndices;
|
||||
private final List<PlayerState> players;
|
||||
|
||||
public BackroomData() {
|
||||
this(DATA_VERSION, List.of(), List.of(), List.of(), List.of());
|
||||
}
|
||||
|
||||
private BackroomData(int dataVersion, List<PendingEntry> pendingEntries, List<LostItem> lostItems,
|
||||
List<ChestState> chests, List<PlayerState> players) {
|
||||
this.dataVersion = Math.max(DATA_VERSION, dataVersion);
|
||||
this.pendingEntries = bounded(pendingEntries, MAX_PENDING_ENTRIES);
|
||||
this.lostItems = bounded(lostItems, MAX_LOST_ITEMS);
|
||||
this.chests = bounded(chests, MAX_CHEST_STATES);
|
||||
this.chestIndices = new HashMap<>();
|
||||
for (int index = 0; index < this.chests.size(); index++) chestIndices.put(this.chests.get(index).pos, index);
|
||||
this.players = bounded(players, MAX_PLAYER_STATES);
|
||||
}
|
||||
|
||||
public static BackroomData get(MinecraftServer server) {
|
||||
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
|
||||
if (overworld == null) throw new IllegalStateException("Overworld unavailable for Backroom data");
|
||||
BackroomData data = overworld.getDataStorage().computeIfAbsent(TYPE);
|
||||
if (data.dataVersion < DATA_VERSION) {
|
||||
data.dataVersion = DATA_VERSION;
|
||||
data.setDirty();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public Optional<PendingEntry> pendingEntry(UUID player) {
|
||||
String id = player.toString();
|
||||
return pendingEntries.stream().filter(entry -> entry.player.equals(id)).findFirst();
|
||||
}
|
||||
|
||||
public Optional<PendingEntry> prepareEntry(UUID player, List<EntrySlot> slots) {
|
||||
pendingEntries.removeIf(entry -> entry.player.equals(player.toString()));
|
||||
if (pendingEntries.size() >= MAX_PENDING_ENTRIES) return Optional.empty();
|
||||
PendingEntry entry = new PendingEntry(player.toString(), UUID.randomUUID().toString(), copySlots(slots));
|
||||
pendingEntries.add(entry);
|
||||
setDirty();
|
||||
return Optional.of(entry);
|
||||
}
|
||||
|
||||
public void finishEntry(UUID player) {
|
||||
if (pendingEntries.removeIf(entry -> entry.player.equals(player.toString()))) setDirty();
|
||||
}
|
||||
|
||||
/** Adds a new loss or returns a previously materialized loss to the pool. */
|
||||
public Optional<UUID> capture(ItemStack source, UUID owner, String cause, long gameTime) {
|
||||
if (source == null || source.isEmpty()) return Optional.empty();
|
||||
ItemStack clean = source.copy();
|
||||
UUID marker = clean.get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
clean.remove(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (marker != null) {
|
||||
int existing = indexOfLoss(marker);
|
||||
if (existing >= 0) {
|
||||
LostItem previous = lostItems.get(existing);
|
||||
lostItems.set(existing, new LostItem(previous.id, owner == null ? previous.owner : owner.toString(),
|
||||
cause, gameTime, clean, LOST_POOL, Long.MIN_VALUE));
|
||||
setDirty();
|
||||
return Optional.of(marker);
|
||||
}
|
||||
}
|
||||
if (lostItems.size() >= MAX_LOST_ITEMS) return Optional.empty();
|
||||
UUID id = marker == null ? UUID.randomUUID() : marker;
|
||||
lostItems.add(new LostItem(id.toString(), owner == null ? "" : owner.toString(), cause,
|
||||
gameTime, clean, LOST_POOL, Long.MIN_VALUE));
|
||||
setDirty();
|
||||
return Optional.of(id);
|
||||
}
|
||||
|
||||
public Optional<MaterializedLoss> takePersonal(UUID owner, long chestPos) {
|
||||
String player = owner.toString();
|
||||
return lostItems.stream()
|
||||
.filter(loss -> loss.state.equals(LOST_POOL) && loss.owner.equals(player))
|
||||
.max(Comparator.comparingLong(LostItem::lostAt))
|
||||
.flatMap(loss -> materialize(loss, chestPos));
|
||||
}
|
||||
|
||||
public Optional<MaterializedLoss> takeShared(long chestPos, long gameTime) {
|
||||
return lostItems.stream()
|
||||
.filter(loss -> loss.state.equals(LOST_POOL)
|
||||
&& sharedEligible(loss.owner, loss.lostAt, gameTime))
|
||||
.min(Comparator.comparingLong(LostItem::lostAt))
|
||||
.flatMap(loss -> materialize(loss, chestPos));
|
||||
}
|
||||
|
||||
static boolean sharedEligible(String owner, long lostAt, long gameTime) {
|
||||
return owner == null || owner.isEmpty()
|
||||
|| gameTime >= lostAt && gameTime - lostAt >= PERSONAL_RESERVATION_TICKS;
|
||||
}
|
||||
|
||||
private Optional<MaterializedLoss> materialize(LostItem loss, long chestPos) {
|
||||
int index = lostItems.indexOf(loss);
|
||||
if (index < 0) return Optional.empty();
|
||||
ItemStack marked = loss.item.copy();
|
||||
UUID id = loss.uuid();
|
||||
marked.set(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT, id);
|
||||
lostItems.set(index, new LostItem(loss.id, loss.owner, loss.cause, loss.lostAt,
|
||||
loss.item.copy(), LOST_CHEST, chestPos));
|
||||
setDirty();
|
||||
return Optional.of(new MaterializedLoss(id, marked));
|
||||
}
|
||||
|
||||
/** Reconciles real items allocated during the chest's previous loot cycle. */
|
||||
public void reconcileChest(long chestPos, List<UUID> markersStillPresent) {
|
||||
List<String> present = markersStillPresent.stream().map(UUID::toString).toList();
|
||||
boolean changed = false;
|
||||
for (int index = lostItems.size() - 1; index >= 0; index--) {
|
||||
LostItem loss = lostItems.get(index);
|
||||
if (!loss.state.equals(LOST_CHEST) || loss.chest != chestPos) continue;
|
||||
if (present.contains(loss.id)) {
|
||||
lostItems.set(index, new LostItem(loss.id, loss.owner, loss.cause, loss.lostAt,
|
||||
loss.item.copy(), LOST_POOL, Long.MIN_VALUE));
|
||||
} else {
|
||||
lostItems.remove(index);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (changed) setDirty();
|
||||
}
|
||||
|
||||
/** Settles a marked item that successfully left the Backroom. */
|
||||
public void settle(UUID lossId) {
|
||||
if (lossId != null && lostItems.removeIf(loss -> loss.id.equals(lossId.toString()))) setDirty();
|
||||
}
|
||||
|
||||
public Optional<ChestState> chest(long pos) {
|
||||
Integer index = chestIndices.get(pos);
|
||||
return index == null ? Optional.empty() : Optional.of(chests.get(index));
|
||||
}
|
||||
|
||||
public void updateChest(long pos, long cycle, List<UUID> assigned) {
|
||||
ChestState replacement = new ChestState(pos, cycle, assigned.stream().limit(54).map(UUID::toString).toList());
|
||||
Integer existing = chestIndices.get(pos);
|
||||
if (existing != null) {
|
||||
chests.set(existing, replacement);
|
||||
} else {
|
||||
if (chests.size() >= MAX_CHEST_STATES) {
|
||||
chests.removeFirst();
|
||||
chestIndices.clear();
|
||||
for (int index = 0; index < chests.size(); index++) chestIndices.put(chests.get(index).pos, index);
|
||||
}
|
||||
chestIndices.put(pos, chests.size());
|
||||
chests.add(replacement);
|
||||
}
|
||||
setDirty();
|
||||
}
|
||||
|
||||
/** Marks a chest and returns whether it is a new personal-recovery attempt. */
|
||||
public ChestVisit visitChest(UUID player, long chestPos) {
|
||||
PlayerState state = playerState(player);
|
||||
ArrayList<Long> visited = new ArrayList<>(state.visitedChests);
|
||||
boolean firstVisit = !visited.contains(chestPos);
|
||||
if (firstVisit) {
|
||||
if (visited.size() >= MAX_VISITED_CHESTS) visited.removeFirst();
|
||||
visited.add(chestPos);
|
||||
state = replacePlayer(state, state.personalAttempts + 1, visited, state.recentArrivals);
|
||||
}
|
||||
return new ChestVisit(firstVisit, state.personalAttempts);
|
||||
}
|
||||
|
||||
public void beginExpedition(UUID player, long packedArrival) {
|
||||
PlayerState state = playerState(player);
|
||||
ArrayList<Long> arrivals = new ArrayList<>(state.recentArrivals);
|
||||
arrivals.remove(packedArrival);
|
||||
arrivals.addFirst(packedArrival);
|
||||
while (arrivals.size() > MAX_RECENT_ARRIVALS) arrivals.removeLast();
|
||||
replacePlayer(state, 0, List.of(), arrivals);
|
||||
}
|
||||
|
||||
public List<Long> recentArrivals(UUID player) {
|
||||
return List.copyOf(playerState(player).recentArrivals);
|
||||
}
|
||||
|
||||
public int lostItemCount() { return lostItems.size(); }
|
||||
public long pooledItemCount() { return lostItems.stream().filter(loss -> loss.state.equals(LOST_POOL)).count(); }
|
||||
public long pooledItemCount(UUID owner) {
|
||||
String id = owner.toString();
|
||||
return lostItems.stream().filter(loss -> loss.state.equals(LOST_POOL) && loss.owner.equals(id)).count();
|
||||
}
|
||||
public int trackedChestCount() { return chests.size(); }
|
||||
|
||||
private PlayerState playerState(UUID player) {
|
||||
String id = player.toString();
|
||||
return players.stream().filter(state -> state.player.equals(id)).findFirst().orElseGet(() -> {
|
||||
if (players.size() >= MAX_PLAYER_STATES) players.removeFirst();
|
||||
PlayerState state = new PlayerState(id, 0, List.of(), List.of());
|
||||
players.add(state);
|
||||
setDirty();
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
private PlayerState replacePlayer(PlayerState old, int attempts, List<Long> visited, List<Long> arrivals) {
|
||||
PlayerState replacement = new PlayerState(old.player, Math.max(0, attempts),
|
||||
bounded(visited, MAX_VISITED_CHESTS), bounded(arrivals, MAX_RECENT_ARRIVALS));
|
||||
players.set(players.indexOf(old), replacement);
|
||||
setDirty();
|
||||
return replacement;
|
||||
}
|
||||
|
||||
private int indexOfLoss(UUID id) {
|
||||
String value = id.toString();
|
||||
for (int index = 0; index < lostItems.size(); index++) if (lostItems.get(index).id.equals(value)) return index;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static List<EntrySlot> copySlots(List<EntrySlot> slots) {
|
||||
return slots.stream().filter(slot -> slot != null && !slot.item.isEmpty()).limit(MAX_ENTRY_SLOTS)
|
||||
.map(slot -> new EntrySlot(slot.location, slot.index, slot.item.copy())).toList();
|
||||
}
|
||||
|
||||
private static <T> ArrayList<T> bounded(List<T> source, int maximum) {
|
||||
List<T> safe = source == null ? List.of() : source;
|
||||
int start = Math.max(0, safe.size() - maximum);
|
||||
return new ArrayList<>(safe.subList(start, safe.size()));
|
||||
}
|
||||
|
||||
public record EntrySlot(String location, int index, ItemStack item) {
|
||||
public EntrySlot { item = item == null ? ItemStack.EMPTY : item.copy(); }
|
||||
}
|
||||
public record PendingEntry(String player, String transaction, List<EntrySlot> slots) {
|
||||
public UUID transactionId() { return UUID.fromString(transaction); }
|
||||
}
|
||||
public record LostItem(String id, String owner, String cause, long lostAt, ItemStack item,
|
||||
String state, long chest) {
|
||||
public UUID uuid() { return UUID.fromString(id); }
|
||||
}
|
||||
public record ChestState(long pos, long cycle, List<String> assigned) {
|
||||
public List<UUID> assignedIds() { return assigned.stream().map(UUID::fromString).toList(); }
|
||||
}
|
||||
private record PlayerState(String player, int personalAttempts, List<Long> visitedChests,
|
||||
List<Long> recentArrivals) {}
|
||||
public record MaterializedLoss(UUID id, ItemStack stack) {}
|
||||
public record ChestVisit(boolean firstVisit, int ordinal) {}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventoryCapacity;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryPlayerProgress;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Exact inventory boundary used by Backroom entry, death and exit. */
|
||||
public final class BackroomInventory {
|
||||
private static final String INVENTORY = "inventory";
|
||||
private static final String OVERFLOW = "overflow";
|
||||
private static final String EGG = "companion_egg";
|
||||
private static final String CAPE = "cape";
|
||||
|
||||
private BackroomInventory() {}
|
||||
|
||||
public static List<BackroomData.EntrySlot> snapshot(ServerPlayer player) {
|
||||
ArrayList<BackroomData.EntrySlot> result = new ArrayList<>();
|
||||
Inventory inventory = player.getInventory();
|
||||
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
|
||||
add(result, INVENTORY, slot, inventory.getItem(slot));
|
||||
}
|
||||
for (int slot = 0; slot < SanctuaryPlayerProgress.OVERFLOW_SLOT_COUNT; slot++) {
|
||||
add(result, OVERFLOW, slot, SanctuaryInventoryCapacity.overflowItem(player, slot));
|
||||
}
|
||||
add(result, EGG, 0, SanctuaryInventoryCapacity.companionEgg(player));
|
||||
add(result, CAPE, 0, SanctuaryInventoryCapacity.equippedCape(player));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<ItemStack> parcel(List<BackroomData.EntrySlot> slots) {
|
||||
return slots.stream().map(BackroomData.EntrySlot::item).filter(stack -> !stack.isEmpty())
|
||||
.map(ItemStack::copy).toList();
|
||||
}
|
||||
|
||||
/** Clears only exact snapshot values, so recovery never destroys a later replacement item. */
|
||||
public static void clearMatching(ServerPlayer player, List<BackroomData.EntrySlot> slots) {
|
||||
for (BackroomData.EntrySlot slot : slots) {
|
||||
ItemStack current = get(player, slot);
|
||||
if (ItemStack.matches(current, slot.item())) set(player, slot, ItemStack.EMPTY);
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
SanctuaryInventoryCapacity.markOverflowChanged(player);
|
||||
}
|
||||
|
||||
public static int captureAll(ServerPlayer player, String cause) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
long now = player.level().getGameTime();
|
||||
int captured = 0;
|
||||
for (BackroomData.EntrySlot slot : snapshot(player)) {
|
||||
if (data.capture(slot.item(), player.getUUID(), cause, now).isEmpty()) continue;
|
||||
set(player, slot, ItemStack.EMPTY);
|
||||
captured++;
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
SanctuaryInventoryCapacity.markOverflowChanged(player);
|
||||
return captured;
|
||||
}
|
||||
|
||||
/** Removes registry markers only when their real item successfully crosses a bed exit. */
|
||||
public static int settleCarriedLosses(ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
int settled = 0;
|
||||
for (BackroomData.EntrySlot slot : snapshot(player)) {
|
||||
ItemStack item = get(player, slot);
|
||||
UUID id = item.get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (id == null) continue;
|
||||
item.remove(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
data.settle(id);
|
||||
set(player, slot, item);
|
||||
settled++;
|
||||
}
|
||||
return settled;
|
||||
}
|
||||
|
||||
private static void add(List<BackroomData.EntrySlot> target, String location, int index, ItemStack stack) {
|
||||
if (stack != null && !stack.isEmpty()) target.add(new BackroomData.EntrySlot(location, index, stack));
|
||||
}
|
||||
|
||||
private static ItemStack get(ServerPlayer player, BackroomData.EntrySlot slot) {
|
||||
return switch (slot.location()) {
|
||||
case INVENTORY -> slot.index() >= 0 && slot.index() < player.getInventory().getContainerSize()
|
||||
? player.getInventory().getItem(slot.index()) : ItemStack.EMPTY;
|
||||
case OVERFLOW -> SanctuaryInventoryCapacity.overflowItem(player, slot.index());
|
||||
case EGG -> SanctuaryInventoryCapacity.companionEgg(player);
|
||||
case CAPE -> SanctuaryInventoryCapacity.equippedCape(player);
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
private static void set(ServerPlayer player, BackroomData.EntrySlot slot, ItemStack stack) {
|
||||
switch (slot.location()) {
|
||||
case INVENTORY -> {
|
||||
if (slot.index() >= 0 && slot.index() < player.getInventory().getContainerSize())
|
||||
player.getInventory().setItem(slot.index(), stack);
|
||||
}
|
||||
case OVERFLOW -> SanctuaryInventoryCapacity.setOverflowItem(player, slot.index(), stack);
|
||||
case EGG -> SanctuaryInventoryCapacity.setCompanionEgg(player, stack);
|
||||
case CAPE -> SanctuaryInventoryCapacity.setEquippedCape(player, stack);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
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 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.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.EntityArgument;
|
||||
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.item.ItemStack;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Bed gateway and crash-safe Mailbox boundary for Backroom expeditions. */
|
||||
public final class BackroomService {
|
||||
public static final int ARRIVAL_RADIUS = 2_000;
|
||||
private static final int ARRIVAL_ATTEMPTS = 48;
|
||||
private static final Set<UUID> AUTHORIZED_TRANSITIONS = new HashSet<>();
|
||||
private static boolean initialized;
|
||||
|
||||
private BackroomService() {}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> reconcile(handler.player));
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||
dispatcher.register(Commands.literal("backroom")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
|
||||
.executes(context -> status(context.getSource()))
|
||||
.then(Commands.literal("status").executes(context -> status(context.getSource())))
|
||||
.then(Commands.literal("inspect")
|
||||
.then(Commands.argument("player", EntityArgument.player())
|
||||
.executes(context -> inspect(context.getSource(), EntityArgument.getPlayer(context, "player"))))));
|
||||
});
|
||||
SanctuaryMod.LOGGER.info("[{}] Backroom bed gateway ready (arrival radius {} blocks).",
|
||||
SanctuaryMod.DISPLAY_NAME, ARRIVAL_RADIUS);
|
||||
}
|
||||
|
||||
/** Returns true when the bed interaction belongs to the Backroom feature. */
|
||||
public static boolean useBed(ServerPlayer player) {
|
||||
if (isBackroom(player.level())) {
|
||||
exit(player);
|
||||
return true;
|
||||
}
|
||||
if (!player.isSecondaryUseActive()) return false;
|
||||
enter(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isBackroom(ServerLevel level) {
|
||||
return level != null && level.dimension().equals(SanctuaryDimensions.BACKROOM);
|
||||
}
|
||||
|
||||
public static boolean enter(ServerPlayer player) {
|
||||
ServerLevel destination = player.level().getServer().getLevel(SanctuaryDimensions.BACKROOM);
|
||||
if (destination == null) {
|
||||
notify(player, "message.sanctuary.backroom.unavailable");
|
||||
return false;
|
||||
}
|
||||
if (!reconcile(player)) {
|
||||
notify(player, "message.sanctuary.backroom.pending");
|
||||
return false;
|
||||
}
|
||||
|
||||
List<BackroomData.EntrySlot> slots = BackroomInventory.snapshot(player);
|
||||
ShopData.DeliveryDestination mailbox = null;
|
||||
if (!slots.isEmpty()) {
|
||||
mailbox = ShopData.get(player.level().getServer()).loadedDeliveryBox(
|
||||
player.level().getServer(), player.getUUID(), level -> !isBackroom(level)).orElse(null);
|
||||
if (mailbox == null) {
|
||||
notify(player, "message.sanctuary.backroom.mailbox_missing");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
BackroomData.PendingEntry pending = data.prepareEntry(player.getUUID(), slots).orElse(null);
|
||||
if (pending == null) {
|
||||
notify(player, "message.sanctuary.backroom.registry_busy");
|
||||
return false;
|
||||
}
|
||||
if (mailbox != null && !mailbox.box().insertTransaction(pending.transactionId(), BackroomInventory.parcel(slots))) {
|
||||
data.finishEntry(player.getUUID());
|
||||
notify(player, "message.sanctuary.backroom.mailbox_full");
|
||||
return false;
|
||||
}
|
||||
BackroomInventory.clearMatching(player, slots);
|
||||
data.finishEntry(player.getUUID());
|
||||
|
||||
BlockPos arrival = chooseArrival(destination, player.getUUID(), data);
|
||||
data.beginExpedition(player.getUUID(), arrival.asLong());
|
||||
TeleportTransition transition = new TeleportTransition(destination, Vec3.atBottomCenterOf(arrival),
|
||||
Vec3.ZERO, player.getYRot(), player.getXRot(),
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
ServerPlayer teleported;
|
||||
AUTHORIZED_TRANSITIONS.add(player.getUUID());
|
||||
try {
|
||||
teleported = player.teleport(transition);
|
||||
} finally {
|
||||
AUTHORIZED_TRANSITIONS.remove(player.getUUID());
|
||||
}
|
||||
if (teleported == null) {
|
||||
SanctuaryMod.LOGGER.error("Backroom teleport failed after Mailbox transaction {} for {}",
|
||||
pending.transaction(), player.getGameProfile().name());
|
||||
return false;
|
||||
}
|
||||
notify(player, "message.sanctuary.backroom.entered");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean exit(ServerPlayer player) {
|
||||
if (!isBackroom(player.level())) return false;
|
||||
TeleportTransition transition = player.findRespawnPositionAndUseSpawnBlock(false,
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
if (isBackroom(transition.newLevel())) {
|
||||
transition = TeleportTransition.createDefault(player,
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
}
|
||||
ServerPlayer teleported;
|
||||
AUTHORIZED_TRANSITIONS.add(player.getUUID());
|
||||
try {
|
||||
teleported = player.teleport(transition);
|
||||
} finally {
|
||||
AUTHORIZED_TRANSITIONS.remove(player.getUUID());
|
||||
}
|
||||
if (teleported == null) return false;
|
||||
int recovered = BackroomInventory.settleCarriedLosses(teleported);
|
||||
notify(teleported, recovered == 0
|
||||
? "message.sanctuary.backroom.exited"
|
||||
: "message.sanctuary.backroom.exited_with_losses");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isAuthorizedTransition(UUID player) {
|
||||
return player != null && AUTHORIZED_TRANSITIONS.contains(player);
|
||||
}
|
||||
|
||||
/** Resolves a crash between Mailbox insertion and inventory clearing. */
|
||||
public static boolean reconcile(ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
BackroomData.PendingEntry pending = data.pendingEntry(player.getUUID()).orElse(null);
|
||||
if (pending == null) return true;
|
||||
ShopData.DeliveryDestination mailbox = ShopData.get(player.level().getServer())
|
||||
.loadedDeliveryBox(player.level().getServer(), player.getUUID(), level -> !isBackroom(level)).orElse(null);
|
||||
if (mailbox == null) return false;
|
||||
if (mailbox.box().hasProcessedTransaction(pending.transactionId())) {
|
||||
BackroomInventory.clearMatching(player, pending.slots());
|
||||
}
|
||||
data.finishEntry(player.getUUID());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BlockPos chooseArrival(ServerLevel level, UUID player, BackroomData data) {
|
||||
RandomSource random = level.getRandom();
|
||||
List<Long> recent = data.recentArrivals(player);
|
||||
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);
|
||||
best = candidate;
|
||||
if (farFromRecent(candidate, recent) && safe(level, candidate)) return candidate;
|
||||
}
|
||||
BlockPos fallback = findSafeNear(level, best == null ? BlockPos.ZERO : best, 32);
|
||||
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);
|
||||
}
|
||||
|
||||
private static boolean farFromRecent(BlockPos candidate, List<Long> recent) {
|
||||
for (long packed : recent) {
|
||||
BlockPos old = BlockPos.of(packed);
|
||||
long dx = old.getX() - candidate.getX(), dz = old.getZ() - candidate.getZ();
|
||||
if (dx * dx + dz * dz < 128L * 128L) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean safe(ServerLevel level, BlockPos feet) {
|
||||
return level.getBlockState(feet).isAir() && level.getBlockState(feet.above()).isAir()
|
||||
&& level.getBlockState(feet.below()).isCollisionShapeFullBlock(level, feet.below());
|
||||
}
|
||||
|
||||
private static BlockPos findSafeNear(ServerLevel level, BlockPos center, int radius) {
|
||||
for (int ring = 0; ring <= radius; ring += 2) {
|
||||
for (int dx = -ring; dx <= ring; dx += 2) for (int dz = -ring; dz <= ring; dz += 2) {
|
||||
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);
|
||||
if (safe(level, candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void notify(ServerPlayer player, String translationKey) {
|
||||
player.sendOverlayMessage(Component.translatable(translationKey));
|
||||
}
|
||||
|
||||
private static int status(CommandSourceStack source) {
|
||||
BackroomData data = BackroomData.get(source.getServer());
|
||||
source.sendSuccess(() -> Component.literal("Backroom: " + data.lostItemCount() + " lost stacks ("
|
||||
+ data.pooledItemCount() + " pooled), " + data.trackedChestCount() + " tracked chest positions; data v"
|
||||
+ BackroomData.DATA_VERSION + "."), false);
|
||||
return data.lostItemCount();
|
||||
}
|
||||
|
||||
private static int inspect(CommandSourceStack source, ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(source.getServer());
|
||||
boolean pending = data.pendingEntry(player.getUUID()).isPresent();
|
||||
source.sendSuccess(() -> Component.literal("Backroom player " + player.getGameProfile().name() + ": "
|
||||
+ data.pooledItemCount(player.getUUID()) + " personal pooled stacks, entry transaction "
|
||||
+ (pending ? "pending" : "clear") + ", " + data.recentArrivals(player.getUUID()).size()
|
||||
+ " remembered arrivals."), false);
|
||||
return pending ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(BedBlock.class)
|
||||
abstract class BedBlockMixin {
|
||||
@Inject(method = "useWithoutItem", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$useBackroomBed(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit, CallbackInfoReturnable<InteractionResult> callback) {
|
||||
if (player instanceof ServerPlayer serverPlayer && BackroomService.useBed(serverPlayer)) {
|
||||
callback.setReturnValue(InteractionResult.SUCCESS_SERVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomChestService;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ContainerUser;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ChestBlockEntity.class)
|
||||
abstract class ChestBlockEntityMixin {
|
||||
@Inject(method = "startOpen", at = @At("HEAD"))
|
||||
private void sanctuary$refreshBackroomChest(ContainerUser user, CallbackInfo callback) {
|
||||
if (user.getLivingEntity() instanceof ServerPlayer player) {
|
||||
BackroomChestService.open((ChestBlockEntity) (Object) this, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomData;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ItemEntity.class)
|
||||
abstract class ItemEntityMixin {
|
||||
@Shadow private @Nullable UUID target;
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$captureLostItem(CallbackInfo callback) {
|
||||
ItemEntity item = (ItemEntity) (Object) this;
|
||||
if (!(item.level() instanceof ServerLevel level) || item.getItem().isEmpty()) return;
|
||||
boolean voidLoss = item.getY() < level.getMinY() - 64.0D;
|
||||
boolean despawn = item.getAge() >= 5_999;
|
||||
if (!voidLoss && !despawn) return;
|
||||
UUID owner = target;
|
||||
Entity thrower = item.getOwner();
|
||||
if (owner == null && thrower instanceof ServerPlayer player) owner = player.getUUID();
|
||||
if (BackroomData.get(level.getServer()).capture(item.getItem(), owner,
|
||||
voidLoss ? "void" : "despawn", level.getGameTime()).isEmpty()) return;
|
||||
item.discard();
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomInventory;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.damagesource.DamageTypes;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(ServerPlayer.class)
|
||||
abstract class ServerPlayerBackroomMixin {
|
||||
@Inject(method = "teleport", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$enforceBackroomExtraction(TeleportTransition transition,
|
||||
CallbackInfoReturnable<ServerPlayer> callback) {
|
||||
ServerPlayer player = (ServerPlayer) (Object) this;
|
||||
boolean leaving = BackroomService.isBackroom(player.level())
|
||||
&& !BackroomService.isBackroom(transition.newLevel());
|
||||
boolean entering = !BackroomService.isBackroom(player.level())
|
||||
&& BackroomService.isBackroom(transition.newLevel());
|
||||
if ((!leaving && !entering) || BackroomService.isAuthorizedTransition(player.getUUID())) return;
|
||||
if (entering) {
|
||||
player.sendOverlayMessage(Component.translatable("message.sanctuary.backroom.bed_entry_only"));
|
||||
callback.setReturnValue(null);
|
||||
return;
|
||||
}
|
||||
BackroomInventory.captureAll(player, "backroom_forced_exit");
|
||||
}
|
||||
|
||||
@Inject(method = "die", at = @At("HEAD"))
|
||||
private void sanctuary$captureBackroomDeath(DamageSource source, CallbackInfo callback) {
|
||||
ServerPlayer player = (ServerPlayer) (Object) this;
|
||||
if (BackroomService.isBackroom(player.level())) {
|
||||
BackroomInventory.captureAll(player, "backroom_death");
|
||||
} else if (source.is(DamageTypes.FELL_OUT_OF_WORLD)) {
|
||||
BackroomInventory.captureAll(player, "void_death");
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -9,6 +9,9 @@ import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestSignItem;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestSignItem;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCapes;
|
||||
import java.util.function.Function;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityType;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
|
||||
@@ -37,6 +40,16 @@ import net.minecraft.world.level.material.MapColor;
|
||||
* Feature registries must be called from here to keep initialization ordering explicit.
|
||||
*/
|
||||
public final class SanctuaryRegistries {
|
||||
public static final Identifier BACKROOM_LOSS_ID = SanctuaryMod.id("backroom_loss_id");
|
||||
public static final DataComponentType<UUID> BACKROOM_LOSS_COMPONENT = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
BACKROOM_LOSS_ID,
|
||||
DataComponentType.<UUID>builder()
|
||||
.persistent(UUIDUtil.CODEC)
|
||||
.networkSynchronized(UUIDUtil.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
public static final Identifier DELIVERY_BOX_ID = SanctuaryMod.id("delivery_box");
|
||||
public static final DeliveryBoxBlock DELIVERY_BOX = registerBlock(
|
||||
DELIVERY_BOX_ID,
|
||||
|
||||
+58
@@ -1,6 +1,10 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
@@ -19,7 +23,9 @@ import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
public final class DeliveryBoxBlockEntity extends BaseContainerBlockEntity {
|
||||
public static final int SIZE = 27;
|
||||
private static final int MAX_PROCESSED_TRANSACTIONS = 128;
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private final Set<UUID> processedTransactions = new LinkedHashSet<>();
|
||||
private UUID owner;
|
||||
|
||||
public DeliveryBoxBlockEntity(BlockPos pos, BlockState state) {
|
||||
@@ -68,16 +74,68 @@ public final class DeliveryBoxBlockEntity extends BaseContainerBlockEntity {
|
||||
return remaining.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically inserts a complete Backroom parcel. Replaying the same transaction is a no-op,
|
||||
* which makes the mailbox side of an entry safe across a server crash.
|
||||
*/
|
||||
public boolean insertTransaction(UUID transactionId, List<ItemStack> deliveries) {
|
||||
if (transactionId == null || deliveries == null) return false;
|
||||
if (processedTransactions.contains(transactionId)) return true;
|
||||
NonNullList<ItemStack> simulated = copyItems(items);
|
||||
for (ItemStack delivery : deliveries) {
|
||||
if (delivery == null || delivery.isEmpty()) continue;
|
||||
if (!insertInto(simulated, delivery.copy())) return false;
|
||||
}
|
||||
items = simulated;
|
||||
processedTransactions.add(transactionId);
|
||||
while (processedTransactions.size() > MAX_PROCESSED_TRANSACTIONS) {
|
||||
processedTransactions.remove(processedTransactions.iterator().next());
|
||||
}
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasProcessedTransaction(UUID transactionId) {
|
||||
return transactionId != null && processedTransactions.contains(transactionId);
|
||||
}
|
||||
|
||||
private static NonNullList<ItemStack> copyItems(List<ItemStack> source) {
|
||||
NonNullList<ItemStack> copy = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
for (int slot = 0; slot < Math.min(SIZE, source.size()); slot++) copy.set(slot, source.get(slot).copy());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static boolean insertInto(NonNullList<ItemStack> target, ItemStack offered) {
|
||||
for (int slot = 0; slot < target.size() && !offered.isEmpty(); slot++) {
|
||||
ItemStack existing = target.get(slot);
|
||||
if (existing.isEmpty() || !ItemStack.isSameItemSameComponents(existing, offered)) continue;
|
||||
int moved = Math.min(offered.getCount(), existing.getMaxStackSize() - existing.getCount());
|
||||
if (moved > 0) { existing.grow(moved); offered.shrink(moved); }
|
||||
}
|
||||
for (int slot = 0; slot < target.size() && !offered.isEmpty(); slot++) {
|
||||
if (!target.get(slot).isEmpty()) continue;
|
||||
int moved = Math.min(offered.getCount(), offered.getMaxStackSize());
|
||||
target.set(slot, offered.copyWithCount(moved));
|
||||
offered.shrink(moved);
|
||||
}
|
||||
return offered.isEmpty();
|
||||
}
|
||||
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
if (owner != null) output.store("Owner", UUIDUtil.CODEC, owner);
|
||||
output.store("BackroomTransactions", UUIDUtil.CODEC.listOf(0, MAX_PROCESSED_TRANSACTIONS),
|
||||
new ArrayList<>(processedTransactions));
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
owner = input.read("Owner", UUIDUtil.CODEC).orElse(null);
|
||||
processedTransactions.clear();
|
||||
input.read("BackroomTransactions", UUIDUtil.CODEC.listOf(0, MAX_PROCESSED_TRANSACTIONS)).orElse(List.of()).stream()
|
||||
.limit(MAX_PROCESSED_TRANSACTIONS).forEach(processedTransactions::add);
|
||||
}
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override protected NonNullList<ItemStack> getItems() { return items; }
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -107,6 +108,11 @@ public final class ShopData extends SavedData {
|
||||
}
|
||||
|
||||
public Optional<DeliveryDestination> loadedDeliveryBox(MinecraftServer server, UUID owner) {
|
||||
return loadedDeliveryBox(server, owner, level -> true);
|
||||
}
|
||||
|
||||
public Optional<DeliveryDestination> loadedDeliveryBox(MinecraftServer server, UUID owner,
|
||||
Predicate<ServerLevel> allowedLevel) {
|
||||
String ownerId = owner.toString();
|
||||
boolean changed = false;
|
||||
Iterator<DeliveryAddress> iterator = deliveryBoxes.iterator();
|
||||
@@ -117,7 +123,7 @@ public final class ShopData extends SavedData {
|
||||
for (ServerLevel candidate : server.getAllLevels()) {
|
||||
if (dimension(candidate).equals(address.dimension)) { level = candidate; break; }
|
||||
}
|
||||
if (level == null) continue;
|
||||
if (level == null || allowedLevel == null || !allowedLevel.test(level)) continue;
|
||||
BlockPos pos = BlockPos.of(address.pos);
|
||||
if (!level.isLoaded(pos)) continue;
|
||||
if (!(level.getBlockEntity(pos) instanceof DeliveryBoxBlockEntity box) || !owner.equals(box.owner())) {
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@ public final class SanctuaryDimensions {
|
||||
public static final ResourceKey<Level> PAINTED_CANYONS = dimension("painted_canyons");
|
||||
public static final ResourceKey<Level> BREAKER_SEA = dimension("breaker_sea");
|
||||
public static final ResourceKey<Level> LAPUTA = dimension("laputa");
|
||||
public static final ResourceKey<Level> BACKROOM = dimension("backroom");
|
||||
|
||||
public static final ResourceKey<LevelStem> MAIN_STEM = stem("main");
|
||||
public static final ResourceKey<LevelStem> OVERWORLD_STEM = stem("overworld");
|
||||
@@ -37,11 +38,12 @@ public final class SanctuaryDimensions {
|
||||
public static final ResourceKey<LevelStem> PAINTED_CANYONS_STEM = stem("painted_canyons");
|
||||
public static final ResourceKey<LevelStem> BREAKER_SEA_STEM = stem("breaker_sea");
|
||||
public static final ResourceKey<LevelStem> LAPUTA_STEM = stem("laputa");
|
||||
public static final ResourceKey<LevelStem> BACKROOM_STEM = stem("backroom");
|
||||
|
||||
public static final Set<Identifier> RESERVED_IDS = Set.of(
|
||||
MAIN.identifier(), OVERWORLD.identifier(), CAVERNS.identifier(), ALPHA.identifier(),
|
||||
SHEEP_LAND.identifier(), SKELETON_WASTE.identifier(), LOST_CITY.identifier(),
|
||||
PAINTED_CANYONS.identifier(), BREAKER_SEA.identifier(), LAPUTA.identifier()
|
||||
PAINTED_CANYONS.identifier(), BREAKER_SEA.identifier(), LAPUTA.identifier(), BACKROOM.identifier()
|
||||
);
|
||||
|
||||
private SanctuaryDimensions() {
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.MatterDepthMaterializ
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.AlphaIndevChunkFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SanctuaryWaterLakeFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SulfurCaveGeologyFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeSelectors;
|
||||
import net.minecraft.core.Registry;
|
||||
@@ -22,6 +23,9 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
|
||||
/** Registers the two generator-specific features migrated from Sanctuary 26.1.2. */
|
||||
public final class SanctuaryFeatures {
|
||||
public static final Identifier BACKROOM_ARCHITECTURE_ID = SanctuaryMod.id("backroom_architecture");
|
||||
public static final Feature<NoneFeatureConfiguration> BACKROOM_ARCHITECTURE =
|
||||
new BackroomArchitectureFeature(NoneFeatureConfiguration.CODEC);
|
||||
public static final Identifier ALPHA_INDEV_CHUNK_ID = SanctuaryMod.id("alpha_indev_chunk");
|
||||
public static final Feature<NoneFeatureConfiguration> ALPHA_INDEV_CHUNK =
|
||||
new AlphaIndevChunkFeature(NoneFeatureConfiguration.CODEC);
|
||||
@@ -56,6 +60,7 @@ public final class SanctuaryFeatures {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
Registry.register(BuiltInRegistries.FEATURE, BACKROOM_ARCHITECTURE_ID, BACKROOM_ARCHITECTURE);
|
||||
Registry.register(BuiltInRegistries.FEATURE, ALPHA_INDEV_CHUNK_ID, ALPHA_INDEV_CHUNK);
|
||||
Registry.register(BuiltInRegistries.FEATURE,
|
||||
MATTER_DEPTH_MATERIALIZATION_ID, MATTER_DEPTH_MATERIALIZATION);
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
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 java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LeavesBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
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.
|
||||
*/
|
||||
public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfiguration> {
|
||||
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 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_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 LEAVES = Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true);
|
||||
|
||||
public BackroomArchitectureFeature(Codec<NoneFeatureConfiguration> codec) {
|
||||
super(codec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(FeaturePlaceContext<NoneFeatureConfiguration> context) {
|
||||
WorldGenLevel level = context.level();
|
||||
int chunkX = Math.floorDiv(context.origin().getX(), CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(context.origin().getZ(), CHUNK_SIZE);
|
||||
int startX = chunkX * CHUNK_SIZE;
|
||||
int startZ = chunkZ * CHUNK_SIZE;
|
||||
if (!withinPlayableArea(startX + 8, startZ + 8)) return false;
|
||||
|
||||
long seed = mix(level.getSeed(), chunkX, chunkZ);
|
||||
Random random = new Random(seed);
|
||||
Architecture architecture = architecture(level.getSeed(), chunkX, chunkZ);
|
||||
switch (architecture) {
|
||||
case CITY -> 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);
|
||||
placeRareAnomaly(level, startX, startZ, random);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean withinPlayableArea(int x, int z) {
|
||||
return (long) x * x + (long) z * z <= (long) PLAYABLE_RADIUS * PLAYABLE_RADIUS;
|
||||
}
|
||||
|
||||
public static Architecture architecture(long worldSeed, int chunkX, int chunkZ) {
|
||||
int macroX = Math.floorDiv(chunkX, MACRO_CHUNKS);
|
||||
int macroZ = Math.floorDiv(chunkZ, MACRO_CHUNKS);
|
||||
int index = Math.floorMod((int) mix(worldSeed ^ 0x4f1bbcdcL, macroX, macroZ), Architecture.values().length);
|
||||
return Architecture.values()[index];
|
||||
}
|
||||
|
||||
/** Stable fingerprint used to prevent palette selection drift. */
|
||||
public static long fingerprint(long worldSeed, int chunkX, int chunkZ) {
|
||||
return mix(worldSeed ^ architecture(worldSeed, chunkX, chunkZ).ordinal(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private static void city(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
int y = 64;
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
boolean road = Math.floorMod(x, 32) < 5 || Math.floorMod(z, 32) < 5;
|
||||
set(level, x, y, z, road ? CONCRETE_GRAY : CONCRETE_LIGHT);
|
||||
if (road && (Math.floorMod(x, 8) == 0 || Math.floorMod(z, 8) == 0)) set(level, x, y, z, Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
}
|
||||
if (Math.floorMod(x0, 32) < 5 || Math.floorMod(z0, 32) < 5) return;
|
||||
int minX = x0 + 2, maxX = x0 + 13, minZ = z0 + 2, maxZ = z0 + 13;
|
||||
int height = 18 + random.nextInt(42);
|
||||
for (int floor = 0; floor <= height; floor++) {
|
||||
int py = y + 1 + floor;
|
||||
boolean slab = floor == 0 || floor == height || floor % 5 == 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;
|
||||
if (slab) set(level, x, py, z, CONCRETE_LIGHT);
|
||||
else if (edge) set(level, x, py, z, floor % 5 == 1 ? CONCRETE_WHITE : GLASS_CYAN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void offices(WorldGenLevel level, int x0, int z0, Random random) {
|
||||
for (int baseY : new int[]{72, 84, 96}) {
|
||||
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
|
||||
set(level, x, baseY, z, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
set(level, x, baseY + 9, z, CLAY_ORANGE);
|
||||
boolean wall = Math.floorMod(x, 12) == 0 || Math.floorMod(z, 12) == 0;
|
||||
if (wall) for (int y = baseY + 1; y < baseY + 9; y++) {
|
||||
BlockState material = y == baseY + 4 ? Blocks.STAINED_GLASS.pick(DyeColor.BROWN).defaultBlockState() : CLAY_BROWN;
|
||||
set(level, x, y, z, material);
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
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, 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void set(WorldGenLevel level, int x, int y, int z, BlockState state) {
|
||||
if (withinPlayableArea(x, z) && y >= level.getMinY() && y < level.getMaxY()) {
|
||||
level.setBlock(new BlockPos(x, y, z), state, 2);
|
||||
}
|
||||
}
|
||||
|
||||
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 Architecture { CITY, OFFICES, PLAINS, AGGREGATES, ROTATED }
|
||||
}
|
||||
@@ -516,6 +516,16 @@
|
||||
"item.sanctuary.blue_scroll_cape": "Blue Scroll Cape",
|
||||
"item.sanctuary.white_cape": "White Cape",
|
||||
"dimension.sanctuary.alpha": "Alpha / Indev Island",
|
||||
"dimension.sanctuary.backroom": "Backroom",
|
||||
"message.sanctuary.backroom.unavailable": "The Backroom is unavailable in this world.",
|
||||
"message.sanctuary.backroom.pending": "Your previous Backroom transfer is waiting for your Mailbox to be loaded.",
|
||||
"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.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.",
|
||||
"screen.sanctuary.atlas.landmark.boss": "Defeated boss",
|
||||
"message.sanctuary.notch_defeated": "Notch Titan has been defeated. The victory is now recorded in this world."
|
||||
}
|
||||
|
||||
@@ -516,6 +516,16 @@
|
||||
"item.sanctuary.blue_scroll_cape": "Cape parchemin bleu",
|
||||
"item.sanctuary.white_cape": "Cape blanche",
|
||||
"dimension.sanctuary.alpha": "Île Alpha / Indev",
|
||||
"dimension.sanctuary.backroom": "Backroom",
|
||||
"message.sanctuary.backroom.unavailable": "La Backroom n’est pas disponible dans ce monde.",
|
||||
"message.sanctuary.backroom.pending": "Ton ancien transfert Backroom attend que ta Mailbox soit chargée.",
|
||||
"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.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.",
|
||||
"screen.sanctuary.atlas.landmark.boss": "Boss vaincu",
|
||||
"message.sanctuary.notch_defeated": "Notch Titan a été vaincu. La victoire est désormais inscrite dans ce monde."
|
||||
}
|
||||
|
||||
@@ -472,6 +472,16 @@
|
||||
"item.sanctuary.blue_scroll_cape": "Плащ синего свитка",
|
||||
"item.sanctuary.white_cape": "Белый плащ",
|
||||
"dimension.sanctuary.alpha": "Остров Альфа / Indev",
|
||||
"dimension.sanctuary.backroom": "Бэкрум",
|
||||
"message.sanctuary.backroom.unavailable": "Бэкрум недоступен в этом мире.",
|
||||
"message.sanctuary.backroom.pending": "Предыдущий перенос в Бэкрум ожидает загрузки вашего почтового ящика.",
|
||||
"message.sanctuary.backroom.registry_busy": "Реестр Бэкрума завершает слишком много прерванных переносов. Повторите попытку позже.",
|
||||
"message.sanctuary.backroom.mailbox_missing": "Загрузите и зарегистрируйте почтовый ящик перед входом в Бэкрум.",
|
||||
"message.sanctuary.backroom.mailbox_full": "В почтовом ящике недостаточно места для снаряжения, яйца спутника и плаща.",
|
||||
"message.sanctuary.backroom.entered": "Вы проваливаетесь сквозь кровать без снаряжения…",
|
||||
"message.sanctuary.backroom.bed_entry_only": "В Бэкрум можно попасть только через кровать.",
|
||||
"message.sanctuary.backroom.exited": "Кровать возвращает вас к точке возрождения со всеми находками.",
|
||||
"message.sanctuary.backroom.exited_with_losses": "Кровать возвращает вас с находками и закрепляет найденные потерянные предметы.",
|
||||
"screen.sanctuary.atlas.landmark.boss": "Побеждённый босс",
|
||||
"message.sanctuary.notch_defeated": "Нотч-Титан побеждён. Победа навсегда записана в этом мире."
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
"settings": "minecraft:overworld"
|
||||
}
|
||||
},
|
||||
"sanctuary:backroom": {
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": { "type": "minecraft:fixed", "biome": "sanctuary:backroom" },
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:fixed",
|
||||
"biome": "sanctuary:backroom"
|
||||
},
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"ambient_light": 0.35,
|
||||
"attributes": {
|
||||
"minecraft:audio/ambient_sounds": {
|
||||
"mood": {
|
||||
"block_search_extent": 8,
|
||||
"offset": 2.0,
|
||||
"sound": "minecraft:ambient.cave",
|
||||
"tick_delay": 5000
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/bed_rule": {
|
||||
"can_set_spawn": "never",
|
||||
"can_sleep": "never"
|
||||
},
|
||||
"minecraft:gameplay/can_start_raid": false,
|
||||
"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/sky_light_factor": 0.0
|
||||
},
|
||||
"cardinal_light": "nether",
|
||||
"coordinate_scale": 1.0,
|
||||
"has_ceiling": false,
|
||||
"has_ender_dragon_fight": false,
|
||||
"has_fixed_time": true,
|
||||
"has_skylight": false,
|
||||
"height": 192,
|
||||
"infiniburn": "#minecraft:infiniburn_overworld",
|
||||
"logical_height": 192,
|
||||
"min_y": 0,
|
||||
"monster_spawn_block_light_limit": 15,
|
||||
"monster_spawn_light_level": 7,
|
||||
"skybox": "none",
|
||||
"timelines": "#minecraft:in_nether"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"temperature": 0.65,
|
||||
"downfall": 0.0,
|
||||
"has_precipitation": false,
|
||||
"attributes": {},
|
||||
"effects": {
|
||||
"water_color": "#596f75",
|
||||
"water_fog_color": "#303b3e",
|
||||
"fog_color": "#b7b9ad",
|
||||
"sky_color": "#b7b9ad",
|
||||
"grass_color": "#70845d",
|
||||
"foliage_color": "#657b58"
|
||||
},
|
||||
"creature_spawn_probability": 0.08,
|
||||
"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 }
|
||||
],
|
||||
"underground_water_creature": [],
|
||||
"water_ambient": [],
|
||||
"water_creature": []
|
||||
},
|
||||
"spawn_costs": {},
|
||||
"carvers": [],
|
||||
"features": [[], [], [], [], ["sanctuary:backroom_architecture"], [], [], [], [], [], []]
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "sanctuary:backroom_architecture",
|
||||
"config": {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"aquifers_enabled": false,
|
||||
"default_block": { "Name": "minecraft:air" },
|
||||
"default_fluid": { "Name": "minecraft:air" },
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": false,
|
||||
"noise": {
|
||||
"height": 192,
|
||||
"min_y": 0,
|
||||
"size_horizontal": 1,
|
||||
"size_vertical": 2
|
||||
},
|
||||
"noise_router": {
|
||||
"barrier": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": -1.0,
|
||||
"fluid_level_floodedness": 0.0,
|
||||
"fluid_level_spread": 0.0,
|
||||
"lava": 0.0,
|
||||
"preliminary_surface_level": 0.0,
|
||||
"ridges": 0.0,
|
||||
"temperature": 0.0,
|
||||
"vegetation": 0.0,
|
||||
"vein_gap": 0.0,
|
||||
"vein_ridged": 0.0,
|
||||
"vein_toggle": 0.0
|
||||
},
|
||||
"ore_veins_enabled": false,
|
||||
"sea_level": 63,
|
||||
"spawn_target": [],
|
||||
"surface_rule": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": { "Name": "minecraft:air" }
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"feature": "sanctuary:backroom_architecture",
|
||||
"placement": [
|
||||
{ "type": "minecraft:count", "count": 1 },
|
||||
{ "type": "minecraft:biome" }
|
||||
]
|
||||
}
|
||||
@@ -58,6 +58,14 @@
|
||||
"settings": "sanctuary:alpha"
|
||||
}
|
||||
},
|
||||
"sanctuary:backroom": {
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": { "type": "minecraft:fixed", "biome": "sanctuary:backroom" },
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
|
||||
@@ -58,6 +58,14 @@
|
||||
"settings": "sanctuary:alpha"
|
||||
}
|
||||
},
|
||||
"sanctuary:backroom": {
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": { "type": "minecraft:fixed", "biome": "sanctuary:backroom" },
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "sanctuary",
|
||||
"data_version": 13,
|
||||
"data_version": 14,
|
||||
"network_protocol": 24,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"AbstractContainerMenuMixin",
|
||||
"BedBlockMixin",
|
||||
"ChestBlockEntityMixin",
|
||||
"ConsumableMixin",
|
||||
"EntityCompanionMixin",
|
||||
"EntityAirSupplyMixin",
|
||||
"FoodDataMixin",
|
||||
"InventoryMixin",
|
||||
"InventoryMenuMixin",
|
||||
"ItemEntityMixin",
|
||||
"ItemTooltipMixin",
|
||||
"LivingEntityCompanionMixin",
|
||||
"PlayerOverflowMixin",
|
||||
"SlotMixin",
|
||||
"ServerGamePacketListenerMixin"
|
||||
"ServerGamePacketListenerMixin",
|
||||
"ServerPlayerBackroomMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.AbstractButtonMixin",
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
|
||||
/** Executable invariants for bounded entry data, personal loot priority and architecture zoning. */
|
||||
public final class BackroomModelSmoke {
|
||||
private BackroomModelSmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
entryTransactionsAreBounded();
|
||||
personalRecoveryIsPrioritized();
|
||||
recentLossesRemainPersonal();
|
||||
architectureIsDeterministicAndComposed();
|
||||
}
|
||||
|
||||
private static void entryTransactionsAreBounded() {
|
||||
BackroomData data = new BackroomData();
|
||||
UUID player = UUID.fromString("c6ff53b1-8b3e-4d7d-9a46-81ef366b02ba");
|
||||
require(BackroomData.MAX_ENTRY_SLOTS == 64, "Entry snapshot bound no longer covers exactly one player inventory");
|
||||
BackroomData.PendingEntry first = data.prepareEntry(player, List.of()).orElseThrow();
|
||||
BackroomData.PendingEntry replacement = data.prepareEntry(player, List.of()).orElseThrow();
|
||||
require(!first.transaction().equals(replacement.transaction()), "Reprepared entry reused its transaction ID");
|
||||
require(data.pendingEntry(player).orElseThrow().slots().isEmpty(),
|
||||
"A player has more than one pending entry transaction");
|
||||
data.finishEntry(player);
|
||||
require(data.pendingEntry(player).isEmpty(), "Finished entry transaction remained persisted");
|
||||
for (int index = 0; index < BackroomData.MAX_PENDING_ENTRIES; index++) {
|
||||
require(data.prepareEntry(new UUID(0L, index + 1L), List.of()).isPresent(),
|
||||
"Pending-entry registry filled before its documented bound");
|
||||
}
|
||||
require(data.prepareEntry(new UUID(1L, 1L), List.of()).isEmpty(),
|
||||
"A new entry evicted an unresolved Mailbox transaction");
|
||||
|
||||
for (int index = 0; index < BackroomData.MAX_RECENT_ARRIVALS + 12; index++) {
|
||||
data.beginExpedition(player, index);
|
||||
}
|
||||
require(data.recentArrivals(player).size() == BackroomData.MAX_RECENT_ARRIVALS,
|
||||
"Arrival history exceeded its persistence bound");
|
||||
}
|
||||
|
||||
private static void personalRecoveryIsPrioritized() {
|
||||
RandomSource first = RandomSource.create(26_200_132L);
|
||||
RandomSource second = RandomSource.create(26_200_133L);
|
||||
RandomSource later = RandomSource.create(26_200_134L);
|
||||
int firstHits = 0, secondHits = 0, laterHits = 0;
|
||||
for (int sample = 0; sample < 20_000; sample++) {
|
||||
if (BackroomChestService.personalRoll(first, 1)) firstHits++;
|
||||
if (BackroomChestService.personalRoll(second, 2)) secondHits++;
|
||||
if (!BackroomChestService.personalRoll(second, 3)) throw new AssertionError("Third personal chest is not guaranteed");
|
||||
if (BackroomChestService.personalRoll(later, 4)) laterHits++;
|
||||
}
|
||||
require(firstHits > 14_500 && firstHits < 15_500, "First-chest personal chance drifted from 75%");
|
||||
require(secondHits > 17_500 && secondHits < 18_500, "Second-chest personal chance drifted from 90%");
|
||||
require(laterHits > 12_500 && laterHits < 13_500, "Later personal chance drifted from 65%");
|
||||
require(BackroomChestService.REFRESH_TICKS == 12_000L, "Chest renewal is no longer twice per Minecraft day");
|
||||
}
|
||||
|
||||
private static void recentLossesRemainPersonal() {
|
||||
require(!BackroomData.sharedEligible("owner", 10_000L,
|
||||
10_000L + BackroomData.PERSONAL_RESERVATION_TICKS - 1L),
|
||||
"A recent owned loss escaped into historical loot");
|
||||
require(BackroomData.sharedEligible("owner", 10_000L,
|
||||
10_000L + BackroomData.PERSONAL_RESERVATION_TICKS),
|
||||
"An old owned loss never became historical loot");
|
||||
require(BackroomData.sharedEligible("", 10_000L, 10_000L),
|
||||
"An ownerless loss was not immediately eligible for shared loot");
|
||||
}
|
||||
|
||||
private static void architectureIsDeterministicAndComposed() {
|
||||
long seed = 26_200_132L;
|
||||
EnumSet<BackroomArchitectureFeature.Architecture> 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(families.size() == BackroomArchitectureFeature.Architecture.values().length,
|
||||
"Not all authored architecture families are reachable");
|
||||
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 require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user