Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Thievery gives criminal activity and property protection a shared set of game me
- **Property locks** — secure supported doors, containers, furniture displays, armour stands, and item frames, with personal, guild, and faction lock rules.
- **Keys and keychains** — carry several keys together and create key copies through molds or paper copies.
- **Lockpicking challenges** — use lockpicks in an interactive timing challenge on doors and displays, or, on chests, a pin memory puzzle in a dialog or a floating lockpick ring followed by a seized-pin probe, with tool strength, lock strength, and Dexterity influencing the attempt.
- **Pickpocketing and robbery** — separate activities provide different targeting, alert, cooldown, and loot-budget rules.
- **Pickpocketing and robbery** — separate activities provide different targeting, alert, cooldown, and loot-budget rules. A pickpocket first fills a floating ring by mashing jump before the pocket opens.
- **Controlled looting** — theft menus account for item categories and value, including equipment quality and magical properties, when determining what can be taken.
- **Evidence and risk** — theft and lockpicking can leave clues, with accumulated risk affecting the chance of more revealing evidence.

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/net/tfminecraft/thievery/Thievery.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void onEnable() {
doorManager = new DoorManager(lockPickManager);
lockMinigameManager = new LockMinigameManager(lockPickManager);
robberyManager = new RobberyManager();
pickpocketManager = new PickpocketManager();
pickpocketManager = new PickpocketManager(lockMinigameManager);
stealGuiUpdater = new StealGuiUpdater(stealManager);
stealGuiUpdater.start();
CooldownResetService cooldownResetService = new CooldownResetService(lockPickManager);
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/net/tfminecraft/thievery/command/CommandManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
+ ThieveryTexts.MUTED + "- Set thievery risk level");
msg(player, ThieveryTexts.WARN + "/thievery itemvalue " + ThieveryTexts.MUTED + "- Inspect held item steal value");
msg(player, ThieveryTexts.WARN + "/thievery keychain " + ThieveryTexts.MUTED + "- Get an empty keychain");
msg(player, ThieveryTexts.WARN + "/thievery testpick [grid|dial] " + ThieveryTexts.MUTED
+ "- Test-pick the container you look at");
msg(player, ThieveryTexts.WARN + "/thievery testpick [grid|dial|pocket] " + ThieveryTexts.MUTED
+ "- Test-pick the container you look at, or a pocket");
}
msg(player, ThieveryTexts.WARN + "/thievery loadout " + ThieveryTexts.MUTED + "- Open thievery category loadout");
msg(player, ThieveryTexts.WARN + "/thievery clearclues " + ThieveryTexts.MUTED + "- Clear clues on a door or container");
Expand Down Expand Up @@ -220,12 +220,18 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
msg(player, ThieveryTexts.ERROR + "You don't have permission to use this command.");
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("pocket")) {
// No mark: the ring plays on its own and nothing opens.
Thievery.getInstance().getLockMinigameManager().startPickpocket(player, null,
() -> msg(player, ThieveryTexts.SUCCESS + "[Thievery] " + ThieveryTexts.WARN + "Pocket picked."));
return true;
}
LockMinigameManager.Mode mode = null;
if (args.length >= 2) {
try {
mode = LockMinigameManager.Mode.valueOf(args[1].toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
msg(player, ThieveryTexts.ERROR + "Usage: " + ThieveryTexts.WARN + "/thievery testpick [grid|dial]");
msg(player, ThieveryTexts.ERROR + "Usage: " + ThieveryTexts.WARN + "/thievery testpick [grid|dial|pocket]");
return true;
}
}
Expand Down Expand Up @@ -257,7 +263,7 @@ public List<String> onTabComplete(CommandSender sender, Command command, String

if (args.length == 2 && args[0].equalsIgnoreCase("testpick") && sender.hasPermission("thievery.admin")) {
String prefix = args[1].toLowerCase(Locale.ROOT);
return List.of("grid", "dial").stream().filter(option -> option.startsWith(prefix)).collect(Collectors.toList());
return List.of("grid", "dial", "pocket").stream().filter(option -> option.startsWith(prefix)).collect(Collectors.toList());
}

if (args.length == 2 && (args[0].equalsIgnoreCase("resetcooldowns") || args[0].equalsIgnoreCase("setrisk"))
Expand Down
39 changes: 33 additions & 6 deletions src/main/java/net/tfminecraft/thievery/door/LockMinigame.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.bukkit.Input;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.Container;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
Expand All @@ -17,14 +18,20 @@
import net.tfminecraft.thievery.utils.ThieveryTexts;

/**
* A chest lock minigame. Subclasses draw and run the puzzle; this class holds the boss bar and the end-of-game
* pause, and reports the outcome to {@link LockMinigameManager}.
* A chest lock or pickpocket minigame. Subclasses draw and run the puzzle; this class holds the boss bar and the
* end-of-game pause, and reports the outcome to {@link LockMinigameManager}.
*/
public abstract class LockMinigame {

static final int SOLVED_TICKS = 15;
static final int FAILED_TICKS = 30;

/** What the thief is told when the attempt is interrupted or ends; bar titles have no full stop. */
record Wording(String flinched, String pulledAway, String gaveUp, String solved, String failed) {}

static final Wording LOCK = new Wording("You flinch and lose the pins.", "You were pulled away from the lock.",
"You ease the pick back out.", "The lock gives way", "The pins slip");

enum Outcome {
NONE,
SOLVED,
Expand All @@ -33,6 +40,7 @@ enum Outcome {

final LockMinigameManager manager;
final UUID playerId;
/** The chest being picked; null for a pickpocket. */
final Block target;
final String targetId;
final IntConsumer onSolved;
Expand Down Expand Up @@ -75,6 +83,21 @@ enum Outcome {
void input(Player player, Input input) {
}

Wording wording() {
return LOCK;
}

/** Whether the chest has gone, so there is nothing left to pick. */
boolean targetGone() {
// No snapshot: a full chest's snapshot copies every item, and this runs every tick.
return !(target.getState(false) instanceof Container);
}

/** A failed attempt: the lock fail cooldown and perhaps a snapped pick. */
void penalise(Player player) {
manager.penalise(player, this);
}

/** Whether the boss bar shows the thief the puzzle's state and countdown. */
boolean showsBar() {
return true;
Expand All @@ -88,17 +111,21 @@ boolean holdsStill() {
void solve(Player player) {
outcome = Outcome.SOLVED;
endTicks = SOLVED_TICKS;
status(ThieveryTexts.SUCCESS + "The lock gives way", BarColor.GREEN);
status(ThieveryTexts.SUCCESS + wording().solved(), BarColor.GREEN);
showOutcome(player);
player.playSound(player.getLocation(), Sound.BLOCK_IRON_TRAPDOOR_OPEN, 0.8f, 1.2f);
player.playSound(player.getLocation(), solvedSound(), 0.8f, 1.2f);
}

void fail(Player player) {
outcome = Outcome.FAILED;
endTicks = FAILED_TICKS;
status(ThieveryTexts.ERROR + "The pins slip", BarColor.RED);
status(ThieveryTexts.ERROR + wording().failed(), BarColor.RED);
showOutcome(player);
manager.penalise(player, this);
penalise(player);
}

Sound solvedSound() {
return Sound.BLOCK_IRON_TRAPDOOR_OPEN;
}

void status(String text, BarColor colour) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.block.Container;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
Expand All @@ -33,13 +32,15 @@

import net.tfminecraft.thievery.Thievery;
import net.tfminecraft.thievery.cache.Parameters;
import net.tfminecraft.thievery.loader.PickpocketLoader;
import net.tfminecraft.thievery.player.RiskCalculator;
import net.tfminecraft.thievery.utils.ThieveryTexts;

/**
* Runs the lock minigame that must be solved before a chest lockpick session opens: the pin grid dialog or, for
* {@code dial-chance} of locks, the floating lockpick ring. Handles the fail cooldown, penalties, and everything
* that can interrupt a pick: leaving, being hit, being moved, a crash, a reload.
* {@code dial-chance} of locks, the floating lockpick ring. Also runs the floating pickpocket ring that must be
* filled before a pocket opens. Handles the fail cooldown, penalties, and everything that can interrupt a pick:
* leaving, being hit, being moved, a crash, a reload.
*/
public class LockMinigameManager implements Listener {

Expand All @@ -49,12 +50,14 @@ public enum Mode {
DIAL
}

private final LockPickManager lockPickManager;
final LockPickManager lockPickManager;
private final Random random;
private final Map<UUID, LockMinigame> games = new HashMap<>();
PinGridGame.GridScreens gridScreens = GridDialogs::frames;
RingDialGame.RingViews ringViews = (player, tumblers, slips) ->
RingView.open(Thievery.getInstance(), player, tumblers, slips);
PickpocketGame.GaugeViews gaugeViews = (player, phases) ->
RingView.openGauge(Thievery.getInstance(), player, phases);

public LockMinigameManager(LockPickManager lockPickManager) {
this(lockPickManager, new Random());
Expand Down Expand Up @@ -98,11 +101,9 @@ public boolean start(Player player, Block target, Mode mode, IntConsumer onSolve
return false;
}
boolean dial = mode == null ? random.nextDouble() < Parameters.chestDialChance : mode == Mode.DIAL;
if (dial && (player.isInsideVehicle() || player.isGliding())) {
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.ERROR + "You need both feet on the ground to work this lock."));
if (dial && !canHoldStill(player, "work this lock")) {
return false;
}
cancel(playerId);

int dexterity = RiskCalculator.getDexterity(player);
LockMinigame game;
Expand All @@ -115,21 +116,59 @@ public boolean start(Player player, Block target, Mode mode, IntConsumer onSolve
+ dexterity * Parameters.chestMinigameRecallSecondsPerDexterity);
game = new PinGridGame(this, playerId, target, targetId, grid, recallTicks, gridScreens, onSolved);
}
play(player, game);
return true;
}

/**
* Opens the pickpocket ring on {@code victim}, then runs {@code onPicked} once it is filled. Runs it straight
* away when the pickpocket minigame is off. A null victim is a staff test, which always plays and alerts no
* one. Returns false when the thief cannot start: still on the fail cooldown for this victim, or riding or
* gliding.
*/
public boolean startPickpocket(Player thief, Player victim, Runnable onPicked) {
if (victim != null && !PickpocketLoader.isMinigameEnabled()) {
onPicked.run();
return true;
}
UUID thiefId = thief.getUniqueId();
String targetId = "pocket:" + (victim == null ? "test" : victim.getUniqueId());
if (lockPickManager.isOnCooldown(thiefId, targetId)) {
thief.sendMessage(ThieveryTexts.msg(ThieveryTexts.ERROR + "Your mark is still on guard. Try again in "
+ lockPickManager.getCooldownRemainingSeconds(thiefId, targetId) + "s."));
return false;
}
if (!canHoldStill(thief, "pick a pocket")) {
return false;
}
play(thief, new PickpocketGame(this, thiefId, targetId, victim, random, gaugeViews, mistakes -> onPicked.run()));
return true;
}

/** The floating rings hold the thief still, which cannot be done on a mount or in flight. */
private static boolean canHoldStill(Player player, String task) {
if (player.isInsideVehicle() || player.isGliding()) {
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.ERROR + "You need both feet on the ground to " + task + "."));
return false;
}
return true;
}

private void play(Player player, LockMinigame game) {
cancel(game.playerId);
game.player = player;
game.pick = player.getInventory().getItemInMainHand().clone();
LockMinigame started = game;
game.task = Bukkit.getScheduler().runTaskTimer(Thievery.getInstance(), () -> tick(player, started), 1L, 1L);
games.put(playerId, game);
game.task = Bukkit.getScheduler().runTaskTimer(Thievery.getInstance(), () -> tick(player, game), 1L, 1L);
games.put(game.playerId, game);
if (game.showsBar()) {
game.bar.addPlayer(player);
}
game.begin(player);
return true;
}

public boolean isPicking(Block target) {
for (LockMinigame game : games.values()) {
if (game.target.equals(target)) {
if (target.equals(game.target)) {
return true;
}
}
Expand Down Expand Up @@ -172,8 +211,7 @@ void tick(Player player, LockMinigame game) {
if (games.get(game.playerId) != game) {
return;
}
// No snapshot: a full chest's snapshot copies every item, and this runs every tick.
if (!player.isOnline() || !(game.target.getState(false) instanceof Container)) {
if (!player.isOnline() || game.targetGone()) {
cancel(game.playerId);
return;
}
Expand All @@ -192,7 +230,7 @@ void tick(Player player, LockMinigame game) {
/** The thief gave up: it counts as a failed attempt, the same as letting the pins slip. */
void giveUp(Player player, LockMinigame game) {
if (games.get(game.playerId) == game && game.outcome == LockMinigame.Outcome.NONE) {
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.MUTED + "You ease the pick back out."));
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.MUTED + game.wording().gaveUp()));
game.fail(player);
}
}
Expand All @@ -216,7 +254,7 @@ public void onQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
LockMinigame game = playing(player);
if (game != null) {
penalise(player, game);
game.penalise(player);
}
LockMinigame any = games.get(player.getUniqueId());
if (any != null) {
Expand All @@ -240,7 +278,7 @@ public void onDamage(EntityDamageEvent event) {
}
LockMinigame game = playing(player);
if (game != null && event.getFinalDamage() > 0) {
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.ERROR + "You flinch and lose the pins."));
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.ERROR + game.wording().flinched()));
game.fail(player);
}
}
Expand All @@ -262,17 +300,17 @@ public void onCommand(PlayerCommandPreprocessEvent event) {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTeleport(PlayerTeleportEvent event) {
Player player = event.getPlayer();
if (!isPlaying(player.getUniqueId())) {
LockMinigame game = games.get(player.getUniqueId());
if (game == null) {
return;
}
Location from = event.getFrom();
Location to = event.getTo();
if (from.getWorld() != to.getWorld() || from.distanceSquared(to) > 1.0) {
LockMinigame running = playing(player);
if (running != null && (running.ranCommand || selfTeleport(event.getCause()))) {
penalise(player, running);
if (game.outcome == LockMinigame.Outcome.NONE && (game.ranCommand || selfTeleport(event.getCause()))) {
game.penalise(player);
}
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.MUTED + "You were pulled away from the lock."));
player.sendMessage(ThieveryTexts.msg(ThieveryTexts.MUTED + game.wording().pulledAway()));
cancel(player.getUniqueId());
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
Loading
Loading