diff --git a/README.md b/README.md index bfe95c1..510a7a1 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ Le fog of war distingue le fond de biomes calculé depuis la seed, volontairemen ## Waystones locaux -I Like To Move It possède désormais `iliketomoveit:waystone`. Chaque joueur doit visiter physiquement une pierre pour la découvrir : seules ses pierres découvertes apparaissent dans le réseau et dans l'Atlas. Dans l'écran, `R` renomme la pierre ouverte, `Entrée` valide et `Échap` annule. Un clic droit ouvre les destinations découvertes de la dimension courante, triées par distance ; le serveur facture un niveau d'XP par tranche commencée de 512 blocs, cherche une arrivée sûre puis applique sons et particules. Deux pierres ou plus sur les mêmes coordonnées X/Z deviennent un ascenseur vertical gratuit et ne se mélangent pas au réseau ordinaire. Sur cet ascenseur, s'accroupir descend vers l'étage découvert immédiatement inférieur et sauter depuis la pierre monte vers l'étage découvert immédiatement supérieur, sans ouvrir l'interface. +I Like To Move It possède désormais `iliketomoveit:waystone`. Chaque joueur doit visiter physiquement une pierre pour la découvrir : seules ses pierres découvertes apparaissent dans le réseau et dans l'Atlas. Dans l'écran, `R` renomme la pierre ouverte, `Entrée` valide et `Échap` annule. Un clic droit ouvre les destinations découvertes de la dimension courante, triées par distance ; le serveur facture un niveau d'XP par tranche commencée de 512 blocs, cherche une arrivée sûre puis applique sons et particules. Deux pierres ou plus sur les mêmes coordonnées X/Z deviennent un ascenseur vertical gratuit et ne se mélangent pas au réseau ordinaire. Sur cet ascenseur, s'accroupir descend vers l'étage immédiatement inférieur même s'il n'était pas encore découvert, puis le découvre à l'arrivée ; sauter depuis la pierre monte uniquement vers l'étage découvert immédiatement supérieur, sans ouvrir l'interface. Le déblocage `waystones` dans Progression est obligatoire hors mode créatif. Le serveur ne fait confiance à aucune distance, aucun coût ni aucune cible envoyés par le client. Les anciennes pierres `waystone:waystone` restent chargeables et l'index `waystone:waystones` est copié une seule fois vers la donnée v2 `iliketomoveit:waystones`, sans réécrire la source. L'ancien JAR Waystone ne doit donc plus être chargé en parallèle, car I Like To Move It réserve son ID historique comme alias de sauvegarde. diff --git a/iliketomoveit/build.gradle b/iliketomoveit/build.gradle index feed3cb..ab804da 100644 --- a/iliketomoveit/build.gradle +++ b/iliketomoveit/build.gradle @@ -8,8 +8,16 @@ tasks.register("grapplePhysicsSmoke", JavaExec) { dependsOn tasks.named("testClasses") } +tasks.register("waystoneElevatorRulesSmoke", JavaExec) { + group = "verification" + description = "Checks directional discovery rules for direct Waystone elevator travel." + classpath = sourceSets.test.runtimeClasspath + mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneElevatorRulesSmoke" + dependsOn tasks.named("testClasses") +} + tasks.named("test") { - // The physics contract is an executable smoke task, not a JUnit engine test. + // Runtime contracts are executable smoke tasks, not JUnit engine tests. failOnNoDiscoveredTests = false } @@ -46,6 +54,10 @@ tasks.register("verifyWaystones") { 'event.key() == 82', 'new WaystoneRenamePayload(source, name)', 'class WaystoneElevatorController', + 'class WaystoneElevatorRules', + 'canUseDirectDestination(upward', + 'canTeleport(elevatorRide, source.getY(), target.getY()', + 'discoveries.discover(player.getUUID(), level, target)', 'player.isShiftKeyDown()', 'player.getDeltaMovement().y > JUMP_VELOCITY_THRESHOLD', 'useElevatorControl(player, supportingStone, false)', @@ -367,6 +379,7 @@ tasks.register("verifyGrapplePrediction") { tasks.named("check") { dependsOn tasks.named("grapplePhysicsSmoke") + dependsOn tasks.named("waystoneElevatorRulesSmoke") dependsOn tasks.named("verifyWaystones") dependsOn tasks.named("verifyGrappling") dependsOn tasks.named("verifyGrapplePrediction") diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java index ad3aa2e..725c4ae 100644 --- a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java @@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.iliketomoveit.network; import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItBlocks; import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneEntry; import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneDiscoveryData; +import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneElevatorRules; import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneSavedData; import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionService; import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryUnlock; @@ -112,16 +113,17 @@ public final class WaystoneTeleportation { WaystoneSyncService.syncNow(player); return; } - if (!discoveries.knows(player.getUUID(), level, source) - || !discoveries.knows(player.getUUID(), level, target)) { - player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.undiscovered")); - return; - } - List entries = data.entriesFor(level); boolean sourceElevator = isElevatorPos(source, entries); boolean targetElevator = isElevatorPos(target, entries); boolean elevatorRide = sourceElevator && targetElevator && isElevatorRide(source, target); + boolean sourceDiscovered = discoveries.knows(player.getUUID(), level, source); + boolean targetDiscovered = discoveries.knows(player.getUUID(), level, target); + if (!WaystoneElevatorRules.canTeleport(elevatorRide, source.getY(), target.getY(), + sourceDiscovered, targetDiscovered)) { + player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.undiscovered")); + return; + } if ((sourceElevator || targetElevator) && !elevatorRide) { player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.elevator_only")); return; @@ -148,6 +150,7 @@ public final class WaystoneTeleportation { player.setDeltaMovement(Vec3.ZERO); player.resetFallDistance(); spawnWarpEffect(level, feet); + if (discoveries.discover(player.getUUID(), level, target)) WaystoneSyncService.syncNow(player); player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.teleported", cost)); } @@ -190,7 +193,8 @@ public final class WaystoneTeleportation { discoveries.discover(player.getUUID(), level, source); Optional target = data.entriesFor(level).stream() .map(WaystoneEntry::pos) - .filter(pos -> discoveries.knows(player.getUUID(), level, pos)) + .filter(pos -> WaystoneElevatorRules.canUseDirectDestination(upward, + discoveries.knows(player.getUUID(), level, pos))) .filter(pos -> pos.getX() == source.getX() && pos.getZ() == source.getZ()) .filter(pos -> upward ? pos.getY() > source.getY() : pos.getY() < source.getY()) .filter(pos -> isWaystone(level, pos)) diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java new file mode 100644 index 0000000..9a9ccbe --- /dev/null +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java @@ -0,0 +1,18 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.waystone; + +/** Directional discovery rules shared by direct controls and authoritative teleport validation. */ +public final class WaystoneElevatorRules { + private WaystoneElevatorRules() { + } + + /** Descending may discover the next floor; ascending still requires prior discovery. */ + public static boolean canUseDirectDestination(boolean upward, boolean targetDiscovered) { + return !upward || targetDiscovered; + } + + /** Only a real downward elevator ride may bypass discovery of its target. */ + public static boolean canTeleport(boolean elevatorRide, int sourceY, int targetY, + boolean sourceDiscovered, boolean targetDiscovered) { + return sourceDiscovered && (targetDiscovered || elevatorRide && targetY < sourceY); + } +} diff --git a/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRulesSmoke.java b/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRulesSmoke.java new file mode 100644 index 0000000..3239f7f --- /dev/null +++ b/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRulesSmoke.java @@ -0,0 +1,33 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.waystone; + +/** Locks the intentional discovery asymmetry between elevator descent and ascent. */ +public final class WaystoneElevatorRulesSmoke { + private WaystoneElevatorRulesSmoke() { + } + + public static void main(String[] args) { + assertAllowed(WaystoneElevatorRules.canUseDirectDestination(false, false), + "descent should select an undiscovered lower floor"); + assertDenied(WaystoneElevatorRules.canUseDirectDestination(true, false), + "ascent should not select an undiscovered upper floor"); + assertAllowed(WaystoneElevatorRules.canUseDirectDestination(true, true), + "ascent should keep selecting discovered upper floors"); + + assertAllowed(WaystoneElevatorRules.canTeleport(true, 80, 48, true, false), + "a real downward elevator ride should accept an undiscovered target"); + assertDenied(WaystoneElevatorRules.canTeleport(true, 48, 80, true, false), + "an upward elevator ride should reject an undiscovered target"); + assertDenied(WaystoneElevatorRules.canTeleport(false, 80, 48, true, false), + "ordinary travel should reject an undiscovered target"); + assertDenied(WaystoneElevatorRules.canTeleport(true, 80, 48, false, false), + "an undiscovered source should never authorize travel"); + } + + private static void assertAllowed(boolean value, String message) { + if (!value) throw new IllegalStateException(message); + } + + private static void assertDenied(boolean value, String message) { + if (value) throw new IllegalStateException(message); + } +}