From 95ad1296569cacbe121ec763b5e2a8487d039689 Mon Sep 17 00:00:00 2001 From: koka Date: Thu, 27 Aug 2026 14:41:22 +0200 Subject: [PATCH] feat(anotherworld): add upright safe vault controls --- CHANGELOG.md | 11 + README.md | 4 +- anotherworld/build.gradle | 88 +++++--- .../anotherworld/AnotherWorldMod.java | 2 +- .../anotherworld/block/SafeVaultBlock.java | 13 +- .../block/entity/SafeVaultBlockEntity.java | 6 +- .../client/AnotherWorldClient.java | 3 + .../anotherworld/client/SafeVaultScreen.java | 67 ++++++ .../registry/AnotherWorldMenus.java | 23 +++ .../registry/AnotherWorldRegistries.java | 1 + .../anotherworld/vault/SafeVaultMenu.java | 193 ++++++++++++++++++ .../anotherworld/vault/VaultData.java | 36 +++- .../anotherworld/vault/VaultPart.java | 46 +++++ .../anotherworld/vault/VaultRules.java | 23 +++ .../anotherworld/vault/VaultService.java | 35 +++- .../anotherworld/vault/VaultStructure.java | 70 +++++-- .../src/main/resources/fabric.mod.json | 2 +- .../anotherworld/vault/VaultRulesSmoke.java | 15 +- build.gradle | 46 ++++- gradle.properties | 2 +- .../anotherworld-safe-vault-data-v3.json | 46 +++++ 21 files changed, 656 insertions(+), 76 deletions(-) create mode 100644 anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java create mode 100644 anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldMenus.java create mode 100644 anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/SafeVaultMenu.java create mode 100644 anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultPart.java create mode 100644 pack/migrations/anotherworld-safe-vault-data-v3.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c35fd..b8780e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve - Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46. - Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées. +## En développement — Safe Block vertical et interface monétaire + +Module modifié : **Another World `0.0.0-alpha.51`**. Le pack distribué reste en **`26.2.0-alpha.202`**. + +- corrige la structure du Safe Block : les neuf segments doivent former un mur vertical exact de 3 × 3, et non une plateforme qui apparaît en 3 × 1 de face ; +- conserve un modèle cubique individuel en main et tant que la structure est incomplète ; +- active uniquement après formation les neuf modèles CTM qui découpent les textures 48×48 du coffre-fort ; +- remplace le coffre vanilla à 27 cases par une interface dédiée à trois soldes centrés — rubis, saphirs et émeraudes — avec les boutons `− ×64`, `− ×1`, `+ ×1` et `+ ×64` ; +- valide tous les transferts côté serveur, utilise seulement les rangées Sanctuary débloquées et refuse un retrait plutôt que de jeter des gemmes lorsque l’inventaire est plein ; +- préserve les soldes existants et le stockage persistant à 27 piles, porte Another World de la donnée v2 à v3 et `safe_vaults` de v1 à v2 ; une ancienne plateforme horizontale doit être reconstruite verticalement. + ## `26.2.0-alpha.202` Modules modifiés : **Another World `0.0.0-alpha.50`**, **It's Alive `0.0.0-alpha.31`**, **I Like To Move It `0.0.0-alpha.14`** et **Sanctuary `0.0.0-alpha.117`**. Les six autres modules conservent leur version issue de l’alpha.201. diff --git a/README.md b/README.md index 2adb03a..5cefdbb 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,11 @@ Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans Version courante du pack local : **`26.2.0-alpha.202`**. Another World passe en **`0.0.0-alpha.50`**, It’s Alive en **`0.0.0-alpha.31`**, I Like To Move It en **`0.0.0-alpha.14`** et Sanctuary en **`0.0.0-alpha.117`** ; les six autres modules conservent leur version de l’alpha.201. +Le développement courant porte Another World en **`0.0.0-alpha.51`** sans republier le pack. Le Safe Block forme désormais un mur vertical exact de 3 × 3 : ses blocs restent des cubes individuels avant validation, puis les neuf segments composent les textures connectées 48×48. Son interface affiche trois soldes centrés — rubis, saphirs et émeraudes — avec les transferts serveur `− ×64`, `− ×1`, `+ ×1` et `+ ×64`. + L’alpha.202 permet de descendre par un ascenseur de Waystones vers l’étage immédiatement inférieur encore inconnu, sans ouvrir la montée ni les trajets ordinaires aux destinations non découvertes. Le Shop et le marché noir comptent et consomment désormais les rubis et saphirs de toutes les rangées débloquées, y compris l’overflow. Le livre culinaire peut enfin être posé sur un pupitre vanilla pour partager le catalogue de recettes de son propriétaire ; retiré du pupitre, il redevient personnel. -Le coffre-fort joueur utilise neuf blocs Another World disposés horizontalement en 3 × 3. Il accepte rubis, saphirs et émeraudes dans 27 emplacements liés au joueur et les rend disponibles aux transactions du Shop même lorsque la structure est déchargée. Une foreuse en titane maintenue pendant 60 secondes permet de piller 20 % du contenu d’un propriétaire connecté, qui reçoit immédiatement une alerte ; une réussite consomme la foreuse et protège ensuite le coffre pendant 24 heures. +Le coffre-fort joueur utilise neuf blocs Another World disposés en mur vertical de 3 × 3. Il conserve les rubis, saphirs et émeraudes dans un compte lié au joueur et les rend disponibles aux transactions du Shop même lorsque la structure est déchargée. Une foreuse en titane maintenue pendant 60 secondes permet de piller 20 % du contenu d’un propriétaire connecté, qui reçoit immédiatement une alerte ; une réussite consomme la foreuse et protège ensuite le coffre pendant 24 heures. L’alpha.201 rend les demandes communautaires réellement terminables. Le demandeur dépose la récompense lors de la publication, le preneur confirme sans commande après avoir lu les détails, KILL progresse automatiquement, BRING livre les objets au demandeur et PAY est confirmé par le joueur désigné. La récompense séquestrée est ensuite expédiée une seule fois dans la Mailbox du preneur. Le pseudonyme conserve la couleur du joueur et le panneau rose adopte une palette plus claire et poudrée. diff --git a/anotherworld/build.gradle b/anotherworld/build.gradle index a0e1090..5cae0f3 100644 --- a/anotherworld/build.gradle +++ b/anotherworld/build.gradle @@ -177,26 +177,54 @@ tasks.register("generateAnotherWorldResources") { // Mirror, creative-only rainbow blocks, gems and first-pass metal art. simpleBlock("mirror", "minecraft:block/cube_all", "anotherworld:block/mirror_glass") - writeJson("assets/anotherworld/blockstates/safe_block.json", [variants: [ - "facing=north": [model: "anotherworld:block/safe_block"], - "facing=east": [model: "anotherworld:block/safe_block", y: 90], - "facing=south": [model: "anotherworld:block/safe_block", y: 180], - "facing=west": [model: "anotherworld:block/safe_block", y: 270] - ]]) - writeJson("assets/anotherworld/models/block/safe_block.json", [ - parent: "minecraft:block/cube", - textures: [ - particle: "anotherworld:block/safe_vault/front", - down: "anotherworld:block/safe_vault/bottom", - up: "anotherworld:block/safe_vault/top", - north: "anotherworld:block/safe_vault/front", - south: "anotherworld:block/safe_vault/side", - west: "anotherworld:block/safe_vault/side", - east: "anotherworld:block/safe_vault/side" - ] + def vaultParts = [ + [id: "bottom_left", column: 0, row: 0], [id: "bottom_center", column: 1, row: 0], + [id: "bottom_right", column: 2, row: 0], [id: "middle_left", column: 0, row: 1], + [id: "center", column: 1, row: 1], [id: "middle_right", column: 2, row: 1], + [id: "top_left", column: 0, row: 2], [id: "top_center", column: 1, row: 2], + [id: "top_right", column: 2, row: 2] + ] + def safeMultipart = [[when: [formed: "false"], apply: [model: "anotherworld:block/safe_block_unformed"]]] + [north: 0, east: 90, south: 180, west: 270].each { facing, rotation -> + vaultParts.each { part -> + def apply = [model: "anotherworld:block/safe_block/${part.id}"] + if (rotation != 0) apply.y = rotation + safeMultipart.add([when: [formed: "true", facing: facing.toString(), part: part.id], apply: apply]) + } + } + writeJson("assets/anotherworld/blockstates/safe_block.json", [multipart: safeMultipart]) + writeJson("assets/anotherworld/models/block/safe_block_unformed.json", [ + parent: "minecraft:block/cube_all", textures: [all: "anotherworld:item/safe_block"] ]) + def third = 16.0D / 3.0D + def segmentUv = { int column, int row -> + [column * third, (2 - row) * third, (column + 1) * third, (3 - row) * third] + } + vaultParts.each { part -> + def frontUv = segmentUv(part.column as int, part.row as int) + def backUv = segmentUv(2 - (part.column as int), part.row as int) + def sideUv = segmentUv(1, part.row as int) + def topUv = segmentUv(part.column as int, 1) + writeJson("assets/anotherworld/models/block/safe_block/${part.id}.json", [ + textures: [ + particle: "anotherworld:block/safe_vault/front", + bottom: "anotherworld:block/safe_vault/bottom", + top: "anotherworld:block/safe_vault/top", + front: "anotherworld:block/safe_vault/front", + side: "anotherworld:block/safe_vault/side" + ], + elements: [[from: [0, 0, 0], to: [16, 16, 16], faces: [ + down: [uv: topUv, texture: "#bottom", cullface: "down"], + up: [uv: topUv, texture: "#top", cullface: "up"], + north: [uv: frontUv, texture: "#front", cullface: "north"], + south: [uv: backUv, texture: "#side", cullface: "south"], + west: [uv: sideUv, texture: "#side", cullface: "west"], + east: [uv: sideUv, texture: "#side", cullface: "east"] + ]]] + ]) + } writeJson("assets/anotherworld/models/item/safe_block.json", [ - parent: "minecraft:item/generated", textures: [layer0: "anotherworld:item/safe_block"] + parent: "anotherworld:block/safe_block_unformed" ]) writeJson("assets/anotherworld/items/safe_block.json", modelItem("anotherworld:item/safe_block")) selfLoot("safe_block") @@ -1594,9 +1622,11 @@ tasks.register("generateAnotherWorldResources") { "container.anotherworld.safe_vault": "Coffre-fort", "tooltip.anotherworld.titanium_drill.use": "Maintenir sur un coffre-fort ennemi pour le forer.", "tooltip.anotherworld.titanium_drill.consumed": "Consommée après un pillage réussi.", - "message.anotherworld.safe_vault.incomplete": "Le coffre-fort exige exactement 9 blocs appartenant au même joueur, posés en 3 × 3.", + "message.anotherworld.safe_vault.incomplete": "Le coffre-fort exige exactement 9 blocs appartenant au même joueur, posés en mur vertical de 3 × 3.", "message.anotherworld.safe_vault.locked": "Ce coffre-fort appartient à un autre joueur.", - "message.anotherworld.safe_vault.formed": "Coffre-fort 3 × 3 lié à vous en %s, %s, %s.", + "message.anotherworld.safe_vault.formed": "Coffre-fort vertical 3 × 3 lié à vous en %s, %s, %s.", + "message.anotherworld.safe_vault.deposit_failed": "Impossible de déposer %s : inventaire vide ou coffre-fort plein.", + "message.anotherworld.safe_vault.withdraw_failed": "Impossible de retirer %s : solde vide ou inventaire plein.", "message.anotherworld.safe_vault.already_owned": "Vous possédez déjà un coffre-fort actif.", "message.anotherworld.safe_vault.break_not_owner": "Seul le propriétaire peut démonter ce bloc de coffre-fort.", "message.anotherworld.safe_vault.empty_before_break": "Videz le coffre-fort et attendez la fin du forage avant de le démonter.", @@ -1616,9 +1646,11 @@ tasks.register("generateAnotherWorldResources") { "container.anotherworld.safe_vault": "Сейф", "tooltip.anotherworld.titanium_drill.use": "Удерживайте на чужом сейфе, чтобы просверлить его.", "tooltip.anotherworld.titanium_drill.consumed": "Расходуется после успешного ограбления.", - "message.anotherworld.safe_vault.incomplete": "Сейф требует ровно 9 блоков одного владельца, уложенных 3 × 3.", + "message.anotherworld.safe_vault.incomplete": "Сейф требует вертикальную стену 3 × 3 из 9 блоков одного владельца.", "message.anotherworld.safe_vault.locked": "Этот сейф принадлежит другому игроку.", - "message.anotherworld.safe_vault.formed": "Сейф 3 × 3 привязан к вам в точке %s, %s, %s.", + "message.anotherworld.safe_vault.formed": "Вертикальный сейф 3 × 3 привязан к вам в точке %s, %s, %s.", + "message.anotherworld.safe_vault.deposit_failed": "Не удалось внести %s: предметов нет или сейф заполнен.", + "message.anotherworld.safe_vault.withdraw_failed": "Не удалось забрать %s: баланс пуст или инвентарь заполнен.", "message.anotherworld.safe_vault.already_owned": "У вас уже есть активный сейф.", "message.anotherworld.safe_vault.break_not_owner": "Разобрать этот блок может только владелец.", "message.anotherworld.safe_vault.empty_before_break": "Опустошите сейф и дождитесь окончания сверления перед разборкой.", @@ -1638,9 +1670,11 @@ tasks.register("generateAnotherWorldResources") { "container.anotherworld.safe_vault": "Safe", "tooltip.anotherworld.titanium_drill.use": "Hold on an enemy safe to drill it.", "tooltip.anotherworld.titanium_drill.consumed": "Consumed after a successful raid.", - "message.anotherworld.safe_vault.incomplete": "A safe requires exactly 9 blocks owned by one player in a 3 × 3 square.", + "message.anotherworld.safe_vault.incomplete": "A safe requires an upright 3 × 3 wall of exactly 9 blocks owned by one player.", "message.anotherworld.safe_vault.locked": "This safe belongs to another player.", - "message.anotherworld.safe_vault.formed": "3 × 3 safe linked to you at %s, %s, %s.", + "message.anotherworld.safe_vault.formed": "Upright 3 × 3 safe linked to you at %s, %s, %s.", + "message.anotherworld.safe_vault.deposit_failed": "Could not deposit %s: none available or the safe is full.", + "message.anotherworld.safe_vault.withdraw_failed": "Could not withdraw %s: the balance is empty or inventory is full.", "message.anotherworld.safe_vault.already_owned": "You already own an active safe.", "message.anotherworld.safe_vault.break_not_owner": "Only the owner may dismantle this safe block.", "message.anotherworld.safe_vault.empty_before_break": "Empty the safe and wait for drilling to end before dismantling it.", @@ -1766,7 +1800,7 @@ tasks.named("sourcesJar") { tasks.register("verifyAnotherWorldCatalog") { group = "verification" - description = "Checks the Another World alpha.50 catalog, player vault and climate-aware biome mixer." + description = "Checks the Another World alpha.51 catalog, player vault and climate-aware biome mixer." dependsOn tasks.named("generateAnotherWorldResources") inputs.file(file("src/main/resources/data/anotherworld/sanctuary_biome_catalog.json")) inputs.file(rootProject.file("sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json")) @@ -1904,7 +1938,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.50" + if (project.version.toString() != "0.0.0-alpha.51" || sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44" || sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2 || !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")') @@ -3221,7 +3255,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.50" + if (project.version.toString() != "0.0.0-alpha.51" || 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" diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java index 37022be..db6f8f3 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java @@ -13,7 +13,7 @@ import org.slf4j.LoggerFactory; public final class AnotherWorldMod implements ModInitializer { public static final String MOD_ID = "anotherworld"; public static final String DISPLAY_NAME = "Another World"; - public static final int CURRENT_DATA_VERSION = 2; + public static final int CURRENT_DATA_VERSION = 3; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); public AnotherWorldMod() { diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java index d9c35bd..323613d 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java @@ -7,6 +7,7 @@ import fr.koka99cab.sanctuary26.anotherworld.vault.VaultData; import fr.koka99cab.sanctuary26.anotherworld.vault.VaultDrillingService; import fr.koka99cab.sanctuary26.anotherworld.vault.VaultService; import fr.koka99cab.sanctuary26.anotherworld.vault.VaultStructure; +import fr.koka99cab.sanctuary26.anotherworld.vault.VaultPart; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.network.chat.Component; @@ -29,15 +30,21 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.phys.BlockHitResult; public final class SafeVaultBlock extends BaseEntityBlock { public static final MapCodec CODEC = simpleCodec(SafeVaultBlock::new); public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty FORMED = BooleanProperty.create("formed"); + public static final EnumProperty PART = EnumProperty.create("part", VaultPart.class); public SafeVaultBlock(Properties properties) { super(properties); - registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + registerDefaultState(stateDefinition.any() + .setValue(FACING, Direction.NORTH) + .setValue(FORMED, false) + .setValue(PART, VaultPart.CENTER)); } @Override protected MapCodec codec() { return CODEC; } @@ -53,7 +60,7 @@ public final class SafeVaultBlock extends BaseEntityBlock { return rotate(state, mirror.getRotation(state.getValue(FACING))); } @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(FACING); + builder.add(FACING, FORMED, PART); } @Override @@ -111,7 +118,7 @@ public final class SafeVaultBlock extends BaseEntityBlock { @Override protected void affectNeighborsAfterRemoval(BlockState state, ServerLevel level, BlockPos pos, boolean movedByPiston) { - if (!movedByPiston) VaultData.get(level.getServer()).unregisterNear(level, pos); + if (!movedByPiston) VaultService.onSegmentRemoved(level, pos, state); super.affectNeighborsAfterRemoval(state, level, pos, movedByPiston); } } diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/entity/SafeVaultBlockEntity.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/entity/SafeVaultBlockEntity.java index 9b45ba2..197a3b3 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/entity/SafeVaultBlockEntity.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/entity/SafeVaultBlockEntity.java @@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.anotherworld.block.entity; import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlockEntities; import fr.koka99cab.sanctuary26.anotherworld.vault.VaultData; +import fr.koka99cab.sanctuary26.anotherworld.vault.SafeVaultMenu; import java.util.UUID; import net.minecraft.core.BlockPos; import net.minecraft.core.UUIDUtil; @@ -11,7 +12,6 @@ import net.minecraft.world.MenuProvider; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.ChestMenu; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; @@ -54,7 +54,7 @@ public final class SafeVaultBlockEntity extends BlockEntity implements MenuProvi @Override public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { if (owner == null || !owner.equals(player.getUUID()) || !(level instanceof ServerLevel serverLevel)) return null; - return ChestMenu.threeRows(containerId, inventory, - VaultData.get(serverLevel.getServer()).container(owner, serverLevel, getBlockPos())); + return new SafeVaultMenu(containerId, inventory, VaultData.get(serverLevel.getServer()), + serverLevel, getBlockPos(), owner); } } diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/AnotherWorldClient.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/AnotherWorldClient.java index 23972e1..cd3f2da 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/AnotherWorldClient.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/AnotherWorldClient.java @@ -4,8 +4,10 @@ import fr.koka99cab.canon.client.CanonClient; import fr.koka99cab.sanctuary26.anotherworld.client.render.MirrorBlockEntityRenderer; import fr.koka99cab.sanctuary26.anotherworld.client.render.AnotherWorldBoatRendering; import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlockEntities; +import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldMenus; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry; +import net.minecraft.client.gui.screens.MenuScreens; public final class AnotherWorldClient implements ClientModInitializer { @Override @@ -14,5 +16,6 @@ public final class AnotherWorldClient implements ClientModInitializer { MiroirClientSettings.load(); AnotherWorldBoatRendering.initialize(); BlockEntityRendererRegistry.register(AnotherWorldBlockEntities.MIRROR, MirrorBlockEntityRenderer::new); + MenuScreens.register(AnotherWorldMenus.SAFE_VAULT, SafeVaultScreen::new); } } diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java new file mode 100644 index 0000000..29d0568 --- /dev/null +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java @@ -0,0 +1,67 @@ +package fr.koka99cab.sanctuary26.anotherworld.client; + +import fr.koka99cab.sanctuary26.anotherworld.vault.SafeVaultMenu; +import fr.koka99cab.sanctuary26.anotherworld.vault.VaultRules; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; + +/** Three centered currency balances with explicit one/stack deposit and withdrawal controls. */ +public final class SafeVaultScreen extends AbstractContainerScreen { + private static final Identifier TEXTURE = Identifier.withDefaultNamespace( + "textures/gui/container/crafting_table.png"); + private static final String[] LABELS = {"− ×64", "− ×1", "+ ×1", "+ ×64"}; + private static final int[] BUTTON_X = {7, 43, 105, 135}; + private static final int[] BUTTON_WIDTH = {34, 30, 28, 34}; + private static final int SLOT_X = 79; + private static final int FIRST_ROW_Y = 17; + private static final int ROW_HEIGHT = 22; + + public SafeVaultScreen(SafeVaultMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + } + + @Override + protected void init() { + super.init(); + titleLabelX = (imageWidth - font.width(title)) / 2; + inventoryLabelY = 72; + for (int currency = 0; currency < VaultRules.CURRENCY_SLOTS; currency++) { + int y = topPos + FIRST_ROW_Y + currency * ROW_HEIGHT; + for (int action = 0; action < VaultRules.BUTTONS_PER_CURRENCY; action++) { + int buttonId = SafeVaultMenu.buttonId(currency, action); + addRenderableWidget(Button.builder(Component.literal(LABELS[action]), + button -> sendTransfer(buttonId)) + .bounds(leftPos + BUTTON_X[action], y, BUTTON_WIDTH[action], 20).build()); + } + } + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) { + super.extractBackground(graphics, mouseX, mouseY, tickDelta); + graphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, leftPos, topPos, 0.0F, 0.0F, + imageWidth, imageHeight, 256, 256); + graphics.fill(leftPos + 5, topPos + 15, leftPos + 171, topPos + 80, 0xFFC6C6C6); + for (int currency = 0; currency < VaultRules.CURRENCY_SLOTS; currency++) { + int x = leftPos + SLOT_X; + int y = topPos + FIRST_ROW_Y + currency * ROW_HEIGHT + 1; + graphics.outline(x, y, 18, 18, 0xFF373737); + graphics.fill(x + 1, y + 1, x + 17, y + 17, 0xFF8B8B8B); + graphics.item(new ItemStack(SafeVaultMenu.currency(currency)), x + 1, y + 1); + Component balance = Component.literal(Integer.toString(menu.balance(currency))); + graphics.text(font, balance, x + 17 - font.width(balance), y + 9, 0xFFFFFFFF, true); + } + } + + private void sendTransfer(int buttonId) { + if (minecraft != null && minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, buttonId); + } + } +} diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldMenus.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldMenus.java new file mode 100644 index 0000000..d58149f --- /dev/null +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldMenus.java @@ -0,0 +1,23 @@ +package fr.koka99cab.sanctuary26.anotherworld.registry; + +import fr.koka99cab.sanctuary26.anotherworld.AnotherWorldMod; +import fr.koka99cab.sanctuary26.anotherworld.vault.SafeVaultMenu; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +public final class AnotherWorldMenus { + public static final MenuType SAFE_VAULT = Registry.register( + BuiltInRegistries.MENU, + AnotherWorldMod.id("safe_vault"), + new MenuType<>(SafeVaultMenu::new, FeatureFlags.VANILLA_SET) + ); + + private AnotherWorldMenus() { + } + + public static void initialize() { + AnotherWorldMod.LOGGER.debug("[{}] Safe-vault menu registered.", AnotherWorldMod.DISPLAY_NAME); + } +} diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldRegistries.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldRegistries.java index 9c2aacb..631dc41 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldRegistries.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldRegistries.java @@ -21,6 +21,7 @@ public final class AnotherWorldRegistries { AnotherWorldEntities.initialize(); AnotherWorldItems.initialize(); AnotherWorldBlockEntities.initialize(); + AnotherWorldMenus.initialize(); AnotherWorldCreativeTab.initialize(); AnotherWorldWorldGeneration.initialize(); AnotherWorldMod.LOGGER.info( diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/SafeVaultMenu.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/SafeVaultMenu.java new file mode 100644 index 0000000..7ef4211 --- /dev/null +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/SafeVaultMenu.java @@ -0,0 +1,193 @@ +package fr.koka99cab.sanctuary26.anotherworld.vault; + +import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldItems; +import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldMenus; +import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView; +import java.util.List; +import java.util.UUID; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** Server-authoritative three-currency vault controls backed by the existing saved account. */ +public final class SafeVaultMenu extends AbstractContainerMenu { + private static final List CURRENCIES = List.of( + AnotherWorldItems.RUBY, + AnotherWorldItems.SAPPHIRE, + Items.EMERALD + ); + + private final ContainerData balances; + private final VaultData vaultData; + private final ServerLevel serverLevel; + private final UUID owner; + private final BlockPos center; + + public SafeVaultMenu(int containerId, Inventory inventory) { + this(containerId, inventory, new SimpleContainerData(VaultRules.CURRENCY_SLOTS), + null, null, null, null); + } + + public SafeVaultMenu(int containerId, Inventory inventory, VaultData data, + ServerLevel level, BlockPos center, UUID owner) { + this(containerId, inventory, new VaultBalanceData(data, owner), data, level, + center.immutable(), owner); + } + + private SafeVaultMenu(int containerId, Inventory inventory, ContainerData balances, + VaultData vaultData, ServerLevel serverLevel, BlockPos center, UUID owner) { + super(AnotherWorldMenus.SAFE_VAULT, containerId); + checkContainerDataCount(balances, VaultRules.CURRENCY_SLOTS); + this.balances = balances; + this.vaultData = vaultData; + this.serverLevel = serverLevel; + this.center = center; + this.owner = owner; + addStandardInventorySlots(inventory, 8, 84); + addDataSlots(balances); + } + + public int balance(int currency) { + return currency >= 0 && currency < VaultRules.CURRENCY_SLOTS ? balances.get(currency) : 0; + } + + public static Item currency(int currency) { + return CURRENCIES.get(Math.clamp(currency, 0, CURRENCIES.size() - 1)); + } + + public static int buttonId(int currency, int action) { + return currency * VaultRules.BUTTONS_PER_CURRENCY + action; + } + + @Override + public boolean clickMenuButton(Player player, int buttonId) { + if (!(player instanceof ServerPlayer serverPlayer) || vaultData == null || !stillValid(player)) return false; + int currencyIndex = VaultRules.buttonCurrency(buttonId); + int amount = VaultRules.buttonAmount(buttonId); + if (currencyIndex < 0 || amount <= 0) return false; + Item item = currency(currencyIndex); + int moved = VaultRules.buttonDeposits(buttonId) + ? deposit(serverPlayer, item, amount) + : withdraw(serverPlayer, item, amount); + if (moved <= 0) { + serverPlayer.sendOverlayMessage(Component.translatable( + VaultRules.buttonDeposits(buttonId) + ? "message.anotherworld.safe_vault.deposit_failed" + : "message.anotherworld.safe_vault.withdraw_failed", + new ItemStack(item).getHoverName())); + } + broadcastChanges(); + return true; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(Player player) { + if (serverLevel == null || vaultData == null || center == null || owner == null) return true; + if (!player.getUUID().equals(owner) + || player.distanceToSqr(center.getX() + 0.5D, center.getY() + 0.5D, center.getZ() + 0.5D) > 64.0D) { + return false; + } + return vaultData.isRegistered(serverLevel, center, owner) + && VaultStructure.find(serverLevel, center) + .filter(formed -> formed.center().equals(center) && formed.owner().equals(owner)).isPresent(); + } + + private int deposit(ServerPlayer player, Item item, int requested) { + int available = countPlayerItem(player, item); + int amount = Math.min(requested, Math.min(available, vaultData.capacity(owner, item))); + if (amount <= 0) return 0; + int removed = removePlayerItem(player, item, amount); + int stored = vaultData.add(owner, item, removed); + if (stored < removed) insertPlayerItem(player, item, removed - stored); + return stored; + } + + private int withdraw(ServerPlayer player, Item item, int requested) { + int amount = Math.min(requested, + Math.min(vaultData.count(owner, item), playerCapacity(player, item))); + if (amount <= 0) return 0; + int removed = vaultData.remove(owner, item, amount); + int inserted = insertPlayerItem(player, item, removed); + if (inserted < removed) vaultData.add(owner, item, removed - inserted); + return inserted; + } + + private static int countPlayerItem(ServerPlayer player, Item item) { + long count = 0L; + for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) { + if (slot.stack().is(item)) count += slot.stack().getCount(); + } + return (int) Math.min(Integer.MAX_VALUE, count); + } + + private static int removePlayerItem(ServerPlayer player, Item item, int amount) { + int remaining = amount; + for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) { + if (remaining <= 0) break; + ItemStack stack = slot.stack(); + if (!stack.is(item)) continue; + int removed = Math.min(remaining, stack.getCount()); + slot.remove(removed); + remaining -= removed; + } + return amount - remaining; + } + + private static int playerCapacity(ServerPlayer player, Item item) { + long capacity = 0L; + for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) { + ItemStack stack = slot.stack(); + if (stack.isEmpty()) capacity += item.getDefaultMaxStackSize(); + else if (stack.is(item)) capacity += Math.max(0, stack.getMaxStackSize() - stack.getCount()); + } + return (int) Math.min(Integer.MAX_VALUE, capacity); + } + + private static int insertPlayerItem(ServerPlayer player, Item item, int amount) { + int remaining = amount; + var slots = SanctuaryPlayerInventoryView.unlockedRows(player); + for (var slot : slots) { + if (remaining <= 0) break; + ItemStack stack = slot.stack(); + if (!stack.is(item) || stack.getCount() >= stack.getMaxStackSize()) continue; + int inserted = Math.min(remaining, stack.getMaxStackSize() - stack.getCount()); + stack.grow(inserted); + slot.markChanged(); + remaining -= inserted; + } + for (var slot : slots) { + if (remaining <= 0) break; + if (!slot.stack().isEmpty()) continue; + int inserted = Math.min(remaining, item.getDefaultMaxStackSize()); + slot.container().setItem(slot.slot(), new ItemStack(item, inserted)); + slot.markChanged(); + remaining -= inserted; + } + return amount - remaining; + } + + private record VaultBalanceData(VaultData data, UUID owner) implements ContainerData { + @Override public int get(int index) { + return index >= 0 && index < CURRENCIES.size() ? data.count(owner, CURRENCIES.get(index)) : 0; + } + @Override public void set(int index, int value) { + } + @Override public int getCount() { + return VaultRules.CURRENCY_SLOTS; + } + } +} diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultData.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultData.java index 9365489..b5cabd9 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultData.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultData.java @@ -26,7 +26,7 @@ import net.minecraft.world.level.saveddata.SavedDataType; /** Global player-linked vault inventory, available to the Shop even when its structure is unloaded. */ public final class VaultData extends SavedData { public static final String SAVE_KEY = "safe_vaults"; - public static final int DATA_VERSION = 1; + public static final int DATA_VERSION = 2; private static final Identifier RUBY = Identifier.fromNamespaceAndPath("anotherworld", "ruby"); private static final Identifier SAPPHIRE = Identifier.fromNamespaceAndPath("anotherworld", "sapphire"); private static final Identifier EMERALD = Identifier.withDefaultNamespace("emerald"); @@ -116,7 +116,8 @@ public final class VaultData extends SavedData { boolean changed = vaults.removeIf(address -> { if (!address.dimension().equals(dimension)) return false; BlockPos center = BlockPos.of(address.pos()); - return center.getY() == member.getY() && Math.abs(center.getX() - member.getX()) <= 1 + return Math.abs(center.getX() - member.getX()) <= 1 + && Math.abs(center.getY() - member.getY()) <= 1 && Math.abs(center.getZ() - member.getZ()) <= 1; }); if (changed) setDirty(); @@ -152,6 +153,37 @@ public final class VaultData extends SavedData { return amount - remaining; } + public int capacity(UUID owner, Item item) { + if (!isCurrency(item)) return 0; + long capacity = 0L; + for (ItemStack stack : items(owner)) { + if (stack.isEmpty()) capacity += item.getDefaultMaxStackSize(); + else if (stack.is(item)) capacity += Math.max(0, stack.getMaxStackSize() - stack.getCount()); + } + return (int) Math.min(Integer.MAX_VALUE, capacity); + } + + public int add(UUID owner, Item item, int amount) { + if (!isCurrency(item) || amount <= 0) return 0; + NonNullList stored = items(owner); + int remaining = amount; + for (ItemStack stack : stored) { + if (remaining <= 0) break; + if (!stack.is(item) || stack.getCount() >= stack.getMaxStackSize()) continue; + int inserted = Math.min(remaining, stack.getMaxStackSize() - stack.getCount()); + stack.grow(inserted); + remaining -= inserted; + } + for (int slot = 0; slot < stored.size() && remaining > 0; slot++) { + if (!stored.get(slot).isEmpty()) continue; + int inserted = Math.min(remaining, item.getDefaultMaxStackSize()); + stored.set(slot, new ItemStack(item, inserted)); + remaining -= inserted; + } + if (remaining != amount) setDirty(); + return amount - remaining; + } + public boolean hasContents(UUID owner) { Account account = accounts.get(owner); return account != null && account.items.stream().anyMatch(stack -> !stack.isEmpty()); diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultPart.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultPart.java new file mode 100644 index 0000000..bfb9b76 --- /dev/null +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultPart.java @@ -0,0 +1,46 @@ +package fr.koka99cab.sanctuary26.anotherworld.vault; + +import net.minecraft.util.StringRepresentable; + +/** Stable position of one segment in the front-facing 3x3 vault wall. */ +public enum VaultPart implements StringRepresentable { + BOTTOM_LEFT("bottom_left", 0, 0), + BOTTOM_CENTER("bottom_center", 1, 0), + BOTTOM_RIGHT("bottom_right", 2, 0), + MIDDLE_LEFT("middle_left", 0, 1), + CENTER("center", 1, 1), + MIDDLE_RIGHT("middle_right", 2, 1), + TOP_LEFT("top_left", 0, 2), + TOP_CENTER("top_center", 1, 2), + TOP_RIGHT("top_right", 2, 2); + + private final String serializedName; + private final int column; + private final int row; + + VaultPart(String serializedName, int column, int row) { + this.serializedName = serializedName; + this.column = column; + this.row = row; + } + + public int column() { + return column; + } + + public int row() { + return row; + } + + public static VaultPart at(int column, int row) { + for (VaultPart part : values()) { + if (part.column == column && part.row == row) return part; + } + throw new IllegalArgumentException("Invalid vault part: " + column + ", " + row); + } + + @Override + public String getSerializedName() { + return serializedName; + } +} diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRules.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRules.java index 9bc9335..9e4d269 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRules.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRules.java @@ -6,6 +6,8 @@ public final class VaultRules { public static final int STRUCTURE_RADIUS = 1; public static final int STRUCTURE_BLOCKS = STRUCTURE_WIDTH * STRUCTURE_WIDTH; public static final int STORAGE_SLOTS = 27; + public static final int CURRENCY_SLOTS = 3; + public static final int BUTTONS_PER_CURRENCY = 4; public static final int RAID_DURATION_TICKS = 60 * 20; public static final int RAID_LOOT_PERCENT = 20; public static final long RAID_PROTECTION_TICKS = 24L * 60L * 60L * 20L; @@ -22,4 +24,25 @@ public final class VaultRules { public static int progressPercent(int elapsedTicks) { return Math.clamp((int) ((long) Math.max(0, elapsedTicks) * 100L / RAID_DURATION_TICKS), 0, 100); } + + public static int buttonCurrency(int buttonId) { + return validButton(buttonId) ? buttonId / BUTTONS_PER_CURRENCY : -1; + } + + public static int buttonAmount(int buttonId) { + if (!validButton(buttonId)) return 0; + return switch (buttonId % BUTTONS_PER_CURRENCY) { + case 0, 3 -> 64; + case 1, 2 -> 1; + default -> 0; + }; + } + + public static boolean buttonDeposits(int buttonId) { + return validButton(buttonId) && buttonId % BUTTONS_PER_CURRENCY >= 2; + } + + private static boolean validButton(int buttonId) { + return buttonId >= 0 && buttonId < CURRENCY_SLOTS * BUTTONS_PER_CURRENCY; + } } diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultService.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultService.java index 12f5a12..3dbcfd8 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultService.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultService.java @@ -11,6 +11,8 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.permissions.Permissions; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; /** Owns vault lifecycle, break protection and Sanctuary currency-storage integration. */ public final class VaultService { @@ -50,18 +52,37 @@ public final class VaultService { } public static void onSegmentPlaced(ServerLevel level, net.minecraft.core.BlockPos pos, ServerPlayer player) { - VaultStructure.find(level, pos).ifPresent(formed -> { - var facing = level.getBlockState(pos).getValue(SafeVaultBlock.FACING); - formed.members().forEach(member -> level.setBlock(member, - level.getBlockState(member).setValue(SafeVaultBlock.FACING, facing), - net.minecraft.world.level.block.Block.UPDATE_CLIENTS)); - tryRegister(level, formed, player); - }); + VaultStructure.find(level, pos).ifPresent(formed -> tryRegister(level, formed, player)); + } + + public static void onSegmentRemoved(ServerLevel level, net.minecraft.core.BlockPos pos, BlockState state) { + VaultData.get(level.getServer()).unregisterNear(level, pos); + if (!state.getValue(SafeVaultBlock.FORMED)) return; + net.minecraft.core.BlockPos center = VaultStructure.centerFor(pos, state); + var facing = state.getValue(SafeVaultBlock.FACING); + var width = facing.getClockWise(); + for (int column = 0; column < VaultRules.STRUCTURE_WIDTH; column++) { + for (int row = 0; row < VaultRules.STRUCTURE_WIDTH; row++) { + var member = center.relative(width, column - VaultRules.STRUCTURE_RADIUS) + .above(row - VaultRules.STRUCTURE_RADIUS); + BlockState memberState = level.getBlockState(member); + if (!memberState.is(AnotherWorldBlocks.SAFE_BLOCK)) continue; + level.setBlock(member, memberState + .setValue(SafeVaultBlock.FORMED, false) + .setValue(SafeVaultBlock.PART, VaultPart.CENTER), Block.UPDATE_CLIENTS); + } + } } public static boolean tryRegister(ServerLevel level, VaultStructure.Formed formed, ServerPlayer player) { VaultData data = VaultData.get(level.getServer()); if (data.register(level, formed.center(), formed.owner())) { + formed.members().forEach(member -> level.setBlock(member, + level.getBlockState(member) + .setValue(SafeVaultBlock.FACING, formed.facing()) + .setValue(SafeVaultBlock.FORMED, true) + .setValue(SafeVaultBlock.PART, formed.part(member)), + Block.UPDATE_CLIENTS)); if (player != null) player.sendSystemMessage(Component.translatable( "message.anotherworld.safe_vault.formed", formed.center().getX(), formed.center().getY(), formed.center().getZ())); diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultStructure.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultStructure.java index f0369e5..9c37745 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultStructure.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultStructure.java @@ -1,37 +1,51 @@ package fr.koka99cab.sanctuary26.anotherworld.vault; import fr.koka99cab.sanctuary26.anotherworld.block.entity.SafeVaultBlockEntity; +import fr.koka99cab.sanctuary26.anotherworld.block.SafeVaultBlock; import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlocks; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.Level; -/** Detects one isolated horizontal 3x3 platform owned by a single player. */ +/** Detects one isolated, upright 3x3 vault wall owned by a single player. */ public final class VaultStructure { private VaultStructure() { } public static Optional find(Level level, BlockPos member) { - if (level == null || member == null) return Optional.empty(); - for (int offsetX = -VaultRules.STRUCTURE_RADIUS; offsetX <= VaultRules.STRUCTURE_RADIUS; offsetX++) { - for (int offsetZ = -VaultRules.STRUCTURE_RADIUS; offsetZ <= VaultRules.STRUCTURE_RADIUS; offsetZ++) { - BlockPos center = member.offset(-offsetX, 0, -offsetZ); - Formed formed = validate(level, center); - if (formed != null && formed.contains(member)) return Optional.of(formed); + if (level == null || member == null || !level.getBlockState(member).is(AnotherWorldBlocks.SAFE_BLOCK)) { + return Optional.empty(); + } + Direction preferred = level.getBlockState(member).getValue(SafeVaultBlock.FACING); + for (Direction facing : List.of(preferred, preferred.getOpposite(), + preferred.getClockWise(), preferred.getCounterClockWise())) { + Direction width = facing.getClockWise(); + for (int offsetWidth = -VaultRules.STRUCTURE_RADIUS; + offsetWidth <= VaultRules.STRUCTURE_RADIUS; offsetWidth++) { + for (int offsetY = -VaultRules.STRUCTURE_RADIUS; + offsetY <= VaultRules.STRUCTURE_RADIUS; offsetY++) { + BlockPos center = member.relative(width, -offsetWidth).below(offsetY); + Formed formed = validate(level, center, facing); + if (formed != null && formed.contains(member)) return Optional.of(formed); + } } } return Optional.empty(); } - private static Formed validate(Level level, BlockPos center) { + private static Formed validate(Level level, BlockPos center, Direction facing) { UUID owner = null; List members = new ArrayList<>(VaultRules.STRUCTURE_BLOCKS); - for (int x = -VaultRules.STRUCTURE_RADIUS; x <= VaultRules.STRUCTURE_RADIUS; x++) { - for (int z = -VaultRules.STRUCTURE_RADIUS; z <= VaultRules.STRUCTURE_RADIUS; z++) { - BlockPos pos = center.offset(x, 0, z); + Direction width = facing.getClockWise(); + for (int column = 0; column < VaultRules.STRUCTURE_WIDTH; column++) { + for (int row = 0; row < VaultRules.STRUCTURE_WIDTH; row++) { + BlockPos pos = center.relative(width, column - VaultRules.STRUCTURE_RADIUS) + .above(row - VaultRules.STRUCTURE_RADIUS); if (!level.getBlockState(pos).is(AnotherWorldBlocks.SAFE_BLOCK) || !(level.getBlockEntity(pos) instanceof SafeVaultBlockEntity blockEntity) || blockEntity.owner() == null) return null; @@ -41,21 +55,35 @@ public final class VaultStructure { } } - // Reject ambiguous 3x4/4x3 carpets: a vault is one discrete 3x3 structure. - for (int x = -2; x <= 2; x++) { - for (int z = -2; z <= 2; z++) { - if (Math.abs(x) <= 1 && Math.abs(z) <= 1) continue; - if (level.getBlockState(center.offset(x, 0, z)).is(AnotherWorldBlocks.SAFE_BLOCK)) return null; + // Reject ambiguous 3x4/4x3 walls: a vault is one discrete upright 3x3 structure. + for (int widthOffset = -2; widthOffset <= 2; widthOffset++) { + for (int yOffset = -2; yOffset <= 2; yOffset++) { + if (Math.abs(widthOffset) <= 1 && Math.abs(yOffset) <= 1) continue; + if (level.getBlockState(center.relative(width, widthOffset).above(yOffset)) + .is(AnotherWorldBlocks.SAFE_BLOCK)) return null; } } - return new Formed(center.immutable(), owner, List.copyOf(members)); + return new Formed(center.immutable(), owner, List.copyOf(members), facing); } - public record Formed(BlockPos center, UUID owner, List members) { + public static BlockPos centerFor(BlockPos member, BlockState state) { + VaultPart part = state.getValue(SafeVaultBlock.PART); + Direction width = state.getValue(SafeVaultBlock.FACING).getClockWise(); + return member.relative(width, VaultRules.STRUCTURE_RADIUS - part.column()) + .below(part.row() - VaultRules.STRUCTURE_RADIUS).immutable(); + } + + public record Formed(BlockPos center, UUID owner, List members, Direction facing) { public boolean contains(BlockPos pos) { - return pos != null && pos.getY() == center.getY() - && Math.abs(pos.getX() - center.getX()) <= VaultRules.STRUCTURE_RADIUS - && Math.abs(pos.getZ() - center.getZ()) <= VaultRules.STRUCTURE_RADIUS; + return pos != null && members.contains(pos); + } + + public VaultPart part(BlockPos pos) { + Direction width = facing.getClockWise(); + int column = (pos.getX() - center.getX()) * width.getStepX() + + (pos.getZ() - center.getZ()) * width.getStepZ() + VaultRules.STRUCTURE_RADIUS; + int row = pos.getY() - center.getY() + VaultRules.STRUCTURE_RADIUS; + return VaultPart.at(column, row); } } } diff --git a/anotherworld/src/main/resources/fabric.mod.json b/anotherworld/src/main/resources/fabric.mod.json index b382ae1..92bad06 100644 --- a/anotherworld/src/main/resources/fabric.mod.json +++ b/anotherworld/src/main/resources/fabric.mod.json @@ -37,7 +37,7 @@ "sanctuary26": { "system_type": "gameplay-system", "namespace": "anotherworld", - "data_version": 2, + "data_version": 3, "network_protocol": 1, "lifecycle": "${lifecycle}", "pack_version": "${pack_version}", diff --git a/anotherworld/src/test/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRulesSmoke.java b/anotherworld/src/test/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRulesSmoke.java index a8bf466..1ed9aa7 100644 --- a/anotherworld/src/test/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRulesSmoke.java +++ b/anotherworld/src/test/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRulesSmoke.java @@ -6,8 +6,21 @@ public final class VaultRulesSmoke { public static void main(String[] args) { require(VaultRules.STRUCTURE_WIDTH == 3, "vault width"); - require(VaultRules.STRUCTURE_BLOCKS == 9, "vault footprint"); + require(VaultRules.STRUCTURE_BLOCKS == 9, "upright vault wall"); require(VaultRules.STORAGE_SLOTS == 27, "vault capacity"); + require(VaultRules.CURRENCY_SLOTS == 3, "currency controls"); + require(VaultPart.at(0, 0) == VaultPart.BOTTOM_LEFT, "bottom-left CTM part"); + require(VaultPart.at(1, 1) == VaultPart.CENTER, "center CTM part"); + require(VaultPart.at(2, 2) == VaultPart.TOP_RIGHT, "top-right CTM part"); + require(VaultRules.buttonCurrency(0) == 0 && VaultRules.buttonCurrency(11) == 2, + "button currency routing"); + require(VaultRules.buttonAmount(0) == 64 && VaultRules.buttonAmount(1) == 1 + && VaultRules.buttonAmount(2) == 1 && VaultRules.buttonAmount(3) == 64, + "button transfer amounts"); + require(!VaultRules.buttonDeposits(1) && VaultRules.buttonDeposits(2), + "button transfer directions"); + require(VaultRules.buttonCurrency(12) == -1 && VaultRules.buttonAmount(-1) == 0, + "invalid button rejection"); require(VaultRules.RAID_DURATION_TICKS == 1_200, "raid duration"); require(VaultRules.RAID_PROTECTION_TICKS == 1_728_000L, "raid protection"); require(VaultRules.raidLoot(0) == 0, "empty loot"); diff --git a/build.gradle b/build.gradle index f265497..305a684 100644 --- a/build.gradle +++ b/build.gradle @@ -1815,7 +1815,7 @@ tasks.register("verifyWorkspace") { || terramixMigration.compatibility?.new_registry_ids != 100 || terramixMigration.compatibility?.worldgen_changed != true || terramixMigration.compatibility?.save_migration_required != false - || anotherWorldManifest?.custom?.sanctuary26?.data_version != 2 + || anotherWorldManifest?.custom?.sanctuary26?.data_version != 3 || anotherWorldManifest?.custom?.sanctuary26?.network_protocol != 1 || sanctuaryManifest?.custom?.sanctuary26?.data_version < 9 ) { @@ -3478,7 +3478,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.202" - || rootProject.anotherworld_version != "0.0.0-alpha.50" + || rootProject.anotherworld_version != "0.0.0-alpha.51" || rootProject.redstoner_version != "0.0.0-alpha.10" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.191" @@ -4585,7 +4585,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { def sanctuaryManifest = new JsonSlurper().parse(file( "sanctuary/src/main/resources/fabric.mod.json")) if (rootProject.pack_version != "26.2.0-alpha.202" - || rootProject.anotherworld_version != "0.0.0-alpha.50" + || rootProject.anotherworld_version != "0.0.0-alpha.51" || rootProject.itsalive_version != "0.0.0-alpha.31" || rootProject.iliketomoveit_version != "0.0.0-alpha.14" || rootProject.sanctuary_version != "0.0.0-alpha.117" @@ -4639,24 +4639,34 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { tasks.register("verifySafeVaultFeature") { group = "verification" - description = "Checks the alpha.202 3x3 player safe, titanium drill and Shop currency-provider contract." + description = "Checks the current upright 3x3 safe, custom currency interface, drill and Shop contract." dependsOn ":anotherworld:generateAnotherWorldResources" inputs.file(file("pack/migrations/anotherworld-safe-vault-data-v2.json")) + inputs.file(file("pack/migrations/anotherworld-safe-vault-data-v3.json")) inputs.file(file("anotherworld/src/main/resources/fabric.mod.json")) inputs.file(file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryCurrencyStorage.java")) inputs.files(fileTree("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault") { include "*.java" }) + inputs.file(file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java")) + inputs.file(file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java")) inputs.files(fileTree("anotherworld/src/main/resources/assets/anotherworld/textures/block/safe_vault") { include "*.png" }) inputs.file(file("anotherworld/src/main/resources/assets/anotherworld/textures/item/safe_block.png")) inputs.file(file("anotherworld/src/main/resources/assets/anotherworld/textures/item/titanium_drill.png")) doLast { def migration = new JsonSlurper().parse(file("pack/migrations/anotherworld-safe-vault-data-v2.json")) + def interfaceMigration = new JsonSlurper().parse(file("pack/migrations/anotherworld-safe-vault-data-v3.json")) def manifest = new JsonSlurper().parse(file("anotherworld/src/main/resources/fabric.mod.json")) def rules = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRules.java").text def data = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultData.java").text + def structure = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultStructure.java").text + def menu = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/SafeVaultMenu.java").text + def block = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java").text + def screen = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java").text def currencyApi = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryCurrencyStorage.java").text def generated = file("anotherworld/build/generated/resources") def safeRecipe = new JsonSlurper().parse(new File(generated, "data/anotherworld/recipe/safe_block.json")) def drillRecipe = new JsonSlurper().parse(new File(generated, "data/anotherworld/recipe/titanium_drill.json")) + def safeBlockState = new JsonSlurper().parse(new File(generated, "assets/anotherworld/blockstates/safe_block.json")) + def safeItemModel = new JsonSlurper().parse(new File(generated, "assets/anotherworld/models/item/safe_block.json")) def requiredTextures = [ "block/safe_vault/bottom.png": [48, 48], "block/safe_vault/front.png": [48, 48], "block/safe_vault/side.png": [48, 48], "block/safe_vault/top.png": [48, 48], @@ -4667,7 +4677,7 @@ tasks.register("verifySafeVaultFeature") { image == null || image.width != size[0] || image.height != size[1] }.keySet() if (rootProject.pack_version != "26.2.0-alpha.202" - || rootProject.anotherworld_version != "0.0.0-alpha.50" + || rootProject.anotherworld_version != "0.0.0-alpha.51" || rootProject.sanctuary_version != "0.0.0-alpha.117" || migration.schema_version != 1 || migration.source?.pack_version != "26.2.0-alpha.201" @@ -4686,14 +4696,34 @@ tasks.register("verifySafeVaultFeature") { || migration.compatibility?.anotherworld_network_protocol != 1 || migration.compatibility?.sanctuary_network_protocol != 21 || migration.compatibility?.pack_release_updated != true + || interfaceMigration.source?.modules != [anotherworld: "0.0.0-alpha.50"] + || interfaceMigration.target?.modules != [anotherworld: "0.0.0-alpha.51"] + || interfaceMigration.safe_vault_structure?.source_footprint != "3x3_horizontal" + || interfaceMigration.safe_vault_structure?.target_footprint != "3x3_upright" + || interfaceMigration.safe_vault_structure?.blocks_required != 9 + || interfaceMigration.safe_vault_structure?.connected_texture_parts != 9 + || interfaceMigration.safe_vault_interface?.currency_slots != 3 + || interfaceMigration.safe_vault_interface?.controls_per_currency != ["withdraw_64", "withdraw_1", "deposit_1", "deposit_64"] + || interfaceMigration.compatibility?.anotherworld_target_data_version != 3 + || interfaceMigration.compatibility?.vault_target_data_version != 2 + || interfaceMigration.compatibility?.saved_currency_balances_preserved != true + || interfaceMigration.compatibility?.pack_release_updated != false || manifest.depends?.sanctuary != ">=0.0.0-alpha.117" - || manifest.custom?.sanctuary26?.data_version != 2 + || manifest.custom?.sanctuary26?.data_version != 3 || !file("build.gradle").text.contains('implementation project(":sanctuary")') || !currencyApi.contains("public interface Provider") || !currencyApi.contains("countAdditional") || !currencyApi.contains("removeAdditional") - || !rules.contains("STRUCTURE_WIDTH = 3") || !rules.contains("RAID_DURATION_TICKS = 60 * 20") + || !rules.contains("STRUCTURE_WIDTH = 3") || !rules.contains("CURRENCY_SLOTS = 3") + || !rules.contains("BUTTONS_PER_CURRENCY = 4") || !rules.contains("RAID_DURATION_TICKS = 60 * 20") || !rules.contains("RAID_LOOT_PERCENT = 20") || !rules.contains("24L * 60L * 60L * 20L") - || !data.contains('SAVE_KEY = "safe_vaults"') || !data.contains("DATA_VERSION = 1") + || !data.contains('SAVE_KEY = "safe_vaults"') || !data.contains("DATA_VERSION = 2") + || !structure.contains("upright 3x3") || !structure.contains(".above(row - VaultRules.STRUCTURE_RADIUS)") + || !block.contains("BooleanProperty FORMED") || !block.contains("EnumProperty PART") + || !menu.contains("clickMenuButton") || !menu.contains("SanctuaryPlayerInventoryView.unlockedRows") + || !screen.contains("handleInventoryButtonClick") || !screen.contains('"− ×64", "− ×1", "+ ×1", "+ ×64"') + || safeBlockState.multipart?.size() != 37 + || safeBlockState.multipart?.count { it.when?.formed == "true" } != 36 + || safeItemModel.parent != "anotherworld:block/safe_block_unformed" || safeRecipe.pattern != ["GGG", "GCG", "GGG"] || drillRecipe.pattern != ["TTT", "RPR", "FDF"] || !invalidTextures.isEmpty()) { diff --git a/gradle.properties b/gradle.properties index 3515c3f..a56ba9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ ambiance_version=0.0.0-alpha.12 ambiance_lifecycle=active redstoner_version=0.0.0-alpha.10 redstoner_lifecycle=active -anotherworld_version=0.0.0-alpha.50 +anotherworld_version=0.0.0-alpha.51 anotherworld_lifecycle=active itsalive_version=0.0.0-alpha.31 itsalive_lifecycle=active diff --git a/pack/migrations/anotherworld-safe-vault-data-v3.json b/pack/migrations/anotherworld-safe-vault-data-v3.json new file mode 100644 index 0000000..6dbf58d --- /dev/null +++ b/pack/migrations/anotherworld-safe-vault-data-v3.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "source": { + "pack_version": "26.2.0-alpha.202", + "modules": { + "anotherworld": "0.0.0-alpha.50" + } + }, + "target": { + "pack_version": "26.2.0-alpha.202", + "modules": { + "anotherworld": "0.0.0-alpha.51" + } + }, + "safe_vault_structure": { + "source_footprint": "3x3_horizontal", + "target_footprint": "3x3_upright", + "blocks_required": 9, + "unformed_blocks_use_individual_cube_model": true, + "connected_48x48_texture_requires_complete_structure": true, + "connected_texture_parts": 9 + }, + "safe_vault_interface": { + "currency_slots": 3, + "currencies": [ + "anotherworld:ruby", + "anotherworld:sapphire", + "minecraft:emerald" + ], + "controls_per_currency": ["withdraw_64", "withdraw_1", "deposit_1", "deposit_64"], + "server_authoritative": true, + "persisted_storage_slots_preserved": 27 + }, + "compatibility": { + "anotherworld_source_data_version": 2, + "anotherworld_target_data_version": 3, + "vault_source_data_version": 1, + "vault_target_data_version": 2, + "anotherworld_network_protocol": 1, + "saved_currency_balances_preserved": true, + "legacy_horizontal_structure_requires_rebuild": true, + "new_registry_ids": ["menu:anotherworld:safe_vault"], + "removed_registry_ids": [], + "pack_release_updated": false + } +} -- 2.54.0