diff --git a/sanctuary/BLACK_MARKET.md b/sanctuary/BLACK_MARKET.md index e642491..c64ddef 100644 --- a/sanctuary/BLACK_MARKET.md +++ b/sanctuary/BLACK_MARKET.md @@ -60,15 +60,20 @@ Les quêtes possèdent leur propre sauvegarde (`black_market_quests`, version 1) panneau actif contient un contrat réel : ajouter des panneaux augmente donc le nombre de quêtes quotidiennes disponibles. -Activation d'un panneau vanilla libre : +Création d'un panneau de quête serveur : -1. Tenir huit gemmes du niveau voulu : émeraude pour facile, rubis pour moyen, saphir - pour difficile. -2. S'accroupir et faire clic droit sur le panneau. Les huit gemmes sont consommées et - le panneau est protégé au nom du joueur qui l'a créé. +1. Fabriquer le panneau dédié du niveau voulu autour d'un panneau vanilla : huit + émeraudes pour facile, huit rubis pour moyen ou huit saphirs pour difficile. +2. Poser ce panneau dédié. Il devient immédiatement un panneau de quête protégé au nom + du joueur qui l'a créé. 3. Le renouvellement a lieu une fois par jour réel, autour de minuit UTC avec un décalage déterministe compris entre zéro et deux heures. +Un panneau vanilla libre ne peut plus être converti en panneau de quête avec des gemmes. +Le panneau rose `sanctuary:quest_request_sign` reste le seul support des demandes +communautaires. Les anciens panneaux vanilla qui possèdent déjà un contrat enregistré +restent utilisables jusqu'à leur destruction afin de préserver les réservations existantes. + Interactions : - clic droit : réserver une quête libre, puis livrer ou vérifier sa propre quête ; diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index e0741b0..703f71e 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -121,7 +121,9 @@ tasks.register("verifyBlackMarket") { || !service.contains("currencyStacks") || !questData.contains('SAVE_KEY = "black_market_quests"') || !quests.contains("ServerLivingEntityEvents.AFTER_DEATH") - || !quests.contains("QuestBoardTier.ACTIVATION_COST") + || !quests.contains("SanctuaryRegistries.questBoardTier") + || quests.contains("activationTier(") + || quests.contains("QuestBoardTier.ACTIVATION_COST") || !quests.contains("addTreasury(claim.depositCurrency(), claim.depositAmount())")) { throw new GradleException("The physical Black Market escrow/delivery contract is incomplete") } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/SanctuaryMod.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/SanctuaryMod.java index 60adf01..40469df 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/SanctuaryMod.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/SanctuaryMod.java @@ -62,8 +62,8 @@ public final class SanctuaryMod implements ModInitializer { SanctuaryProgressionGates.initialize(); SanctuaryPlayerColors.initialize(); ShopService.initialize(); - // Community request signs must claim their interactions before the generic - // gem activation callback used by ordinary signs. + // Community request signs must claim their interactions before the other + // physical sign services. QuestRequestService.initialize(); QuestBoardService.initialize(); BlackMarketService.initialize(); diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/registry/SanctuaryRegistries.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/registry/SanctuaryRegistries.java index d5d09c5..6114958 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/registry/SanctuaryRegistries.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/registry/SanctuaryRegistries.java @@ -99,6 +99,13 @@ public final class SanctuaryRegistries { return java.util.List.of(EMERALD_QUEST_SIGN, RUBY_QUEST_SIGN, SAPPHIRE_QUEST_SIGN); } + public static QuestBoardTier questBoardTier(Block block) { + for (QuestSignSet signs : questSigns()) { + if (block == signs.standing() || block == signs.wall()) return signs.tier(); + } + return null; + } + public static boolean isQuestRequestSign(Block block) { return block == QUEST_REQUEST_SIGN.standing() || block == QUEST_REQUEST_SIGN.wall(); } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java index 2ded32f..0d30899 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java @@ -1,6 +1,7 @@ package fr.koka99cab.sanctuary26.sanctuary.shop; import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView; +import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries; import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; @@ -50,14 +51,9 @@ public final class QuestBoardService { UseBlockCallback.EVENT.register((player, level, hand, hit) -> { if (hand != InteractionHand.MAIN_HAND || !(level.getBlockEntity(hit.getBlockPos()) instanceof SignBlockEntity sign)) return InteractionResult.PASS; - QuestBoardTier heldTier = activationTier(player.getMainHandItem()); - if (level.isClientSide()) { - return player.isSecondaryUseActive() && heldTier != null - && player.getMainHandItem().getCount() >= QuestBoardTier.ACTIVATION_COST - ? InteractionResult.SUCCESS : InteractionResult.PASS; - } + if (level.isClientSide()) return InteractionResult.PASS; return player instanceof ServerPlayer serverPlayer - ? interact(serverPlayer, (ServerLevel) level, hit.getBlockPos(), sign, heldTier) + ? interact(serverPlayer, (ServerLevel) level, hit.getBlockPos(), sign) : InteractionResult.PASS; }); PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> { @@ -81,34 +77,19 @@ public final class QuestBoardService { } private static InteractionResult interact(ServerPlayer player, ServerLevel level, BlockPos pos, - SignBlockEntity sign, QuestBoardTier heldTier) { + SignBlockEntity sign) { QuestBoardData data = QuestBoardData.get(level.getServer()); QuestBoardData.Board board = data.boardAt(dimension(level), pos.asLong()).orElse(null); - if (board != null) { - handleBoard(player, level, sign, data, board); - return InteractionResult.SUCCESS_SERVER; - } - if (!player.isSecondaryUseActive() || heldTier == null - || player.getMainHandItem().getCount() < QuestBoardTier.ACTIVATION_COST) return InteractionResult.PASS; - if (BlackMarketData.get(level.getServer()).listingAt(dimension(level), pos.asLong()).isPresent()) { - message(player, "occupied"); return InteractionResult.CONSUME; - } - if (BlackMarketData.get(level.getServer()).isBanned(player.getUUID())) { - player.sendSystemMessage(Component.translatable("message.sanctuary.black_market.banned")); - return InteractionResult.CONSUME; - } - ItemStack held = player.getMainHandItem(); - held.shrink(QuestBoardTier.ACTIVATION_COST); - player.getInventory().setChanged(); - if (!createBoard(player, level, pos, sign, heldTier)) { - held.grow(QuestBoardTier.ACTIVATION_COST); message(player, "occupied"); return InteractionResult.CONSUME; - } + if (board == null) return InteractionResult.PASS; + handleBoard(player, level, sign, data, board); return InteractionResult.SUCCESS_SERVER; } /** Activates one of the three crafted quest signs without charging its material cost twice. */ public static void activatePlacedSign(ServerPlayer player, ServerLevel level, BlockPos pos, SignBlockEntity sign, QuestBoardTier tier) { + QuestBoardTier placedTier = SanctuaryRegistries.questBoardTier(level.getBlockState(pos).getBlock()); + if (!canActivateDedicatedSign(placedTier, tier)) return; if (BlackMarketData.get(level.getServer()).isBanned(player.getUUID())) { player.sendSystemMessage(Component.translatable("message.sanctuary.black_market.banned")); return; @@ -116,6 +97,10 @@ public final class QuestBoardService { if (!createBoard(player, level, pos, sign, tier)) message(player, "occupied"); } + static boolean canActivateDedicatedSign(QuestBoardTier placedTier, QuestBoardTier requestedTier) { + return placedTier != null && placedTier == requestedTier; + } + private static boolean createBoard(ServerPlayer player, ServerLevel level, BlockPos pos, SignBlockEntity sign, QuestBoardTier tier) { QuestBoardData data = QuestBoardData.get(level.getServer()); @@ -466,13 +451,6 @@ public final class QuestBoardService { if (item != null) player.getInventory().placeItemBackInInventory(new ItemStack(item, deposit.amount)); } - private static QuestBoardTier activationTier(ItemStack stack) { - if (stack.isEmpty()) return null; - Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem()); - for (QuestBoardTier tier : QuestBoardTier.values()) if (tier.currencyItem().equals(id)) return tier; - return null; - } - private static ServerPlayer killer(LivingEntity entity, DamageSource source) { Entity attacker = source.getEntity(); if (attacker instanceof ServerPlayer player) return player; diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardTier.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardTier.java index bcad216..e159e63 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardTier.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardTier.java @@ -5,13 +5,12 @@ import java.util.Locale; import net.minecraft.ChatFormatting; import net.minecraft.resources.Identifier; -/** Physical quest-board tiers. The activation material is also the default refundable bond. */ +/** Physical quest-board tiers. Each tier currency is also its default refundable bond. */ public enum QuestBoardTier { EMERALD("emerald", Identifier.withDefaultNamespace("emerald"), 1, Duration.ofMinutes(15), ChatFormatting.GREEN), RUBY("ruby", Identifier.fromNamespaceAndPath("anotherworld", "ruby"), 1, Duration.ofMinutes(45), ChatFormatting.RED), SAPPHIRE("sapphire", Identifier.fromNamespaceAndPath("anotherworld", "sapphire"), 1, Duration.ofMinutes(90), ChatFormatting.AQUA); - public static final int ACTIVATION_COST = 8; private final String id; private final Identifier currencyItem; private final int deposit; diff --git a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardModelSmoke.java b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardModelSmoke.java index 87489a4..7c701e0 100644 --- a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardModelSmoke.java +++ b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardModelSmoke.java @@ -29,6 +29,12 @@ public final class QuestBoardModelSmoke { require(reserved.withObjective(objective.advance(20)).objective().complete(), "Kill progress did not complete"); require(QuestBoardTier.SAPPHIRE.reservationMillis() > QuestBoardTier.RUBY.reservationMillis(), "Difficulty deadline order is invalid"); + require(!QuestBoardService.canActivateDedicatedSign(null, QuestBoardTier.EMERALD), + "A vanilla sign can still become a quest board"); + require(!QuestBoardService.canActivateDedicatedSign(QuestBoardTier.EMERALD, QuestBoardTier.RUBY), + "A mismatched dedicated sign accepted the wrong quest tier"); + require(QuestBoardService.canActivateDedicatedSign(QuestBoardTier.SAPPHIRE, QuestBoardTier.SAPPHIRE), + "A dedicated quest sign rejected its own tier"); long now = 1_800_000_000_000L; long refresh = QuestBoardGenerator.nextRefreshAt("board", now); require(refresh > now && refresh - now < 28L * 60L * 60L * 1_000L,