Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

> Follow the signal, find the source, and uncover loot in TF-Minecraft.

Geiger Counters turns exploration into a treasure hunt. Each character has their own cache buried a few hundred blocks away. A signal bar, particle rings and clicking lead them to it, and a close reading shows where to dig. Opening the cache gives loot and drains the counter, and the next counter starts a new hunt near wherever the character is.
Geiger Counters turns exploration into a treasure hunt. Each character has their own cache buried a few hundred blocks away, or in global mode everyone hunts one shared cache. A signal bar, particle rings and clicking lead them to it, and a close reading shows where to dig. Opening the cache gives loot and drains the counter, and the next counter starts a new hunt near wherever the character is.

## Features

- **A source per character** — each character's cache is buried near them and kept until they dig it up.
- **Global mode** — `/geiger mode global` switches to one cache anywhere in the area, shared by every hunter; the first to open it takes the loot.
- **Buried caches** — the cache is a chest only its hunter sees, and only while holding a live detector.
- **Signal bar** — a boss bar fills as the player closes in and peaks when they face the source; close by it shows where to dig.
- **Visual tracking** — particle rings change in color and number as the player approaches the source.
Expand Down
168 changes: 143 additions & 25 deletions src/main/java/net/tfminecraft/geigercounters/commands/GeigerCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import net.tfminecraft.geigercounters.config.GeigerConfiguration;
import net.tfminecraft.geigercounters.config.GeigerConfiguration.HuntMode;
import net.tfminecraft.geigercounters.config.Messages;
import net.tfminecraft.geigercounters.handlers.SourceHandler;
import net.tfminecraft.geigercounters.managers.GeigerManager;
import net.tfminecraft.geigercounters.utils.Hunters;
import net.tfminecraft.geigercounters.utils.Utils;
Expand All @@ -29,11 +31,12 @@

// ====================================
// Admin command handler:
// /geiger <locate [player]|move <player> [x z]|limits <player>|resetlimits <player>|droplist [name]|reload>
// /geiger <locate [player]|move <player> [x z]|mode [player|global]|limits <player>|resetlimits <player>|droplist [name]|reload>
// In global mode locate and move act on the shared source: /geiger locate, /geiger move [x z]
// ====================================
public class GeigerCommand implements CommandExecutor, TabCompleter, Listener {

private static final List<String> SUBCOMMANDS = Arrays.asList("locate", "move", "limits", "resetlimits", "droplist", "reload");
private static final List<String> SUBCOMMANDS = Arrays.asList("locate", "move", "mode", "limits", "resetlimits", "droplist", "reload");

private final GeigerManager manager;
private final Map<UUID, String> knownNames = new LinkedHashMap<>();
Expand Down Expand Up @@ -72,6 +75,9 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
case "move":
handleMove(sender, args);
return true;
case "mode":
handleMode(sender, args);
return true;
// Singular spellings are aliases - both read naturally
case "limit":
case "limits":
Expand All @@ -96,6 +102,10 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
// /geiger locate -> the sender's own source
// /geiger locate <player> -> that player's current character's source
private void handleLocate(CommandSender sender, String[] args) {
if (isGlobal()) {
locateShared(sender);
return;
}
if (args.length > 2 || (args.length == 1 && !(sender instanceof Player))) {
sender.sendMessage(messages().get("admin.locate-usage"));
return;
Expand Down Expand Up @@ -129,24 +139,18 @@ private void handleLocate(CommandSender sender, String[] args) {
// /geiger move <player> -> clear it; a new one is placed near them on their next hold
// /geiger move <player> <x> <z> -> buried under these coordinates
private void handleMove(CommandSender sender, String[] args) {
if (isGlobal()) {
moveShared(sender, args);
return;
}
if (args.length != 2 && args.length != 4) {
sender.sendMessage(messages().get("admin.move-usage"));
return;
}

double x = 0;
double z = 0;
if (args.length == 4) {
try {
x = Double.parseDouble(args[2]);
z = Double.parseDouble(args[3]);
if (!Double.isFinite(x) || !Double.isFinite(z)) {
throw new NumberFormatException("Coordinates must be finite");
}
} catch (NumberFormatException e) {
sender.sendMessage(messages().get("admin.move-invalid-coords"));
return;
}
double[] coords = args.length == 4 ? parseCoordinates(sender, args[2], args[3]) : null;
if (args.length == 4 && coords == null) {
return;
}

Player target = onlineTarget(sender, args[1]);
Expand All @@ -162,10 +166,107 @@ private void handleMove(CommandSender sender, String[] args) {
}

String name = target.getName();
manager.getSourceHandler().placeAt(hunter, x, z)
manager.getSourceHandler().placeAt(hunter, coords[0], coords[1])
.thenAccept(location -> sendNewLocation(sender, name, location));
}

// The X and Z typed, or null after telling the sender they are not numbers
private double[] parseCoordinates(CommandSender sender, String rawX, String rawZ) {
try {
double x = Double.parseDouble(rawX);
double z = Double.parseDouble(rawZ);
if (!Double.isFinite(x) || !Double.isFinite(z)) {
throw new NumberFormatException("Coordinates must be finite");
}
return new double[] {x, z};
} catch (NumberFormatException e) {
sender.sendMessage(messages().get("admin.move-invalid-coords"));
return null;
}
}

private boolean isGlobal() {
return manager.getConfiguration().getHuntMode() == HuntMode.GLOBAL;
}

// /geiger locate in global mode -> the shared source, whoever asks
private void locateShared(CommandSender sender) {
SourceHandler sources = manager.getSourceHandler();
if (sources.isPlacing(SourceHandler.SHARED)) {
sender.sendMessage(messages().get("admin.shared-source-relocating"));
return;
}

Location source = sources.getSource(SourceHandler.SHARED);
if (source == null) {
sender.sendMessage(messages().get("admin.shared-no-source"));
return;
}

sender.sendMessage(messages().get("admin.shared-source-located",
"%x%", source.getBlockX(),
"%y%", source.getBlockY(),
"%z%", source.getBlockZ(),
"%world%", source.getWorld().getName()));
}

// /geiger move -> clear the shared source; a new one is buried on the next hold
// /geiger move <x z> -> bury it under these coordinates
private void moveShared(CommandSender sender, String[] args) {
if (args.length != 1 && args.length != 3) {
sender.sendMessage(messages().get("admin.shared-move-usage"));
return;
}

if (args.length == 1) {
manager.getSourceHandler().clear(SourceHandler.SHARED);
sender.sendMessage(messages().get("admin.shared-move-cleared"));
return;
}

double[] coords = parseCoordinates(sender, args[1], args[2]);
if (coords == null) {
return;
}
manager.getSourceHandler().placeAt(SourceHandler.SHARED, coords[0], coords[1])
.thenAccept(location -> sendSharedLocation(sender, location));
}

private void sendSharedLocation(CommandSender sender, Location source) {
if (source == null) {
sender.sendMessage(messages().get("admin.move-failed"));
return;
}

sender.sendMessage(messages().get("admin.shared-move-success",
"%x%", source.getBlockX(),
"%y%", source.getBlockY(),
"%z%", source.getBlockZ()));
}

// /geiger mode -> which mode is in use
// /geiger mode <mode> -> switch to it, saved to config.yml. Each mode keeps
// its own sources, so switching back resumes them.
private void handleMode(CommandSender sender, String[] args) {
if (args.length == 1) {
sender.sendMessage(messages().get("admin.mode-current",
"%mode%", manager.getConfiguration().getHuntMode().key()));
return;
}

HuntMode mode = args.length == 2 ? HuntMode.parse(args[1]) : null;
if (mode == null) {
sender.sendMessage(messages().get("admin.mode-usage"));
return;
}

manager.getPlugin().getConfig().set("source.mode", mode.key());
manager.getPlugin().saveConfig();
manager.reload();

sender.sendMessage(messages().get("admin.mode-changed", "%mode%", mode.key()));
}

// Sources belong to characters, so the target has to be online to know which one
private Player onlineTarget(CommandSender sender, String name) {
Player target = Bukkit.getPlayerExact(name);
Expand Down Expand Up @@ -303,11 +404,9 @@ private void sendUsage(CommandSender sender) {

// ====================================
// Suggest coordinates worth typing: where the sender is standing, and the
// corners of the configured search area. Axis follows the argument -
// X for the first, Z for the second.
// corners of the configured search area, on the X axis or the Z.
// ====================================
private List<String> completeCoordinate(CommandSender sender, String[] args) {
boolean isX = args.length == 3;
private List<String> completeCoordinate(CommandSender sender, String[] args, boolean isX) {
String typed = args[args.length - 1];

List<String> suggestions = new ArrayList<>();
Expand Down Expand Up @@ -354,9 +453,26 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
return matches;
}

// Coordinates for /geiger move <player> <x> <z>
if (args[0].equalsIgnoreCase("move") && (args.length == 3 || args.length == 4)) {
return completeCoordinate(sender, args);
// Coordinates for /geiger move <player> <x> <z>, or /geiger move <x> <z> in global mode
if (args[0].equalsIgnoreCase("move")) {
int first = isGlobal() ? 2 : 3;
if (args.length == first || args.length == first + 1) {
return completeCoordinate(sender, args, args.length == first);
}
if (isGlobal()) {
return new ArrayList<>();
}
}

// /geiger mode <player|global>
if (args.length == 2 && args[0].equalsIgnoreCase("mode")) {
List<String> matches = new ArrayList<>();
for (HuntMode mode : HuntMode.values()) {
if (mode.key().startsWith(args[1].toLowerCase(Locale.ROOT))) {
matches.add(mode.key());
}
}
return matches;
}

// Drop list names for /geiger droplist <name>
Expand All @@ -370,8 +486,10 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
return matches;
}

// locate, move and both limit subcommands (either spelling) take a player name
if (args.length == 2 && takesPlayer(args[0])) {
// locate, move and both limit subcommands (either spelling) take a player name;
// in global mode locate takes none
boolean sharedLocate = isGlobal() && args[0].equalsIgnoreCase("locate");
if (args.length == 2 && takesPlayer(args[0]) && !sharedLocate) {
Map<UUID, String> names = new LinkedHashMap<>();
if (args[0].equalsIgnoreCase("limit") || args[0].equalsIgnoreCase("limits")
|| args[0].equalsIgnoreCase("resetlimit") || args[0].equalsIgnoreCase("resetlimits")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public class ConfigMigrator {
// v5: drops.tiers -> drops.lists.<name>.tiers, selected by drops.active-list
// v6: a source per character - source distances, bar section, detection range capped
// v7: buried caches - burial depths, pinpoint range, ring moved out, collection-distance gone
public static final int CURRENT_VERSION = 7;
// v8: source.mode (player or global), detection range back to 2500
public static final int CURRENT_VERSION = 8;

public static final String MESSAGES_FILE = "messages.yml";

Expand Down Expand Up @@ -85,12 +86,12 @@ public void migrate() {
boolean movedDropList = migrateDropLists(live);

List<String> addedKeys = copyMissingKeys(packaged, live);
if (liveVersion < 6) {
capDetectionRange(live);
}
if (liveVersion < 7) {
moveRingOut(live);
}
if (liveVersion < 8) {
raiseDefault(live, "detection.max-detection-distance", 1000.0, 2500.0);
}
boolean versionChanged = liveVersion != CURRENT_VERSION;

if (addedKeys.isEmpty() && !versionChanged && !movedDropList) {
Expand Down Expand Up @@ -225,23 +226,6 @@ private boolean migrateDropLists(FileConfiguration live) {
return true;
}

// ====================================
// A detection range sized for one source anywhere on the map makes the
// clicks and rings read "close" for most of a nearby hunt. Sources now sit
// within reroll-distance, so the range is capped at that once.
// ====================================
private void capDetectionRange(FileConfiguration live) {
double reroll = live.getDouble("source.reroll-distance");
double detection = live.getDouble("detection.max-detection-distance");
if (detection <= reroll) {
return;
}

live.set("detection.max-detection-distance", reroll);
plugin.getLogger().info("Lowered detection.max-detection-distance from " + detection + " to " + reroll
+ " to match source.reroll-distance.");
}

// ====================================
// v6 shipped a 150-500 block ring, which proved too close. Only values
// still at those defaults move; a ring the admin chose is kept. Caches are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -45,6 +46,9 @@ public class GeigerConfiguration {
private World world;
private double minX, maxX, minZ, maxZ;

// Whether each character hunts their own source or everyone shares one
private HuntMode huntMode;

// Where each character's source is placed, relative to the character
private double minSourceDistance;
private double maxSourceDistance;
Expand Down Expand Up @@ -108,6 +112,7 @@ public GeigerConfiguration(JavaPlugin plugin) {
public void load() {
loadWorld();
loadSearchArea();
loadHuntMode();
loadSourceDistances();
loadSpawnFilters();
loadDetectionSettings();
Expand Down Expand Up @@ -143,6 +148,15 @@ private void loadSearchArea() {
}
}

private void loadHuntMode() {
String raw = plugin.getConfig().getString("source.mode", HuntMode.PLAYER.key());
huntMode = HuntMode.parse(raw);
if (huntMode == null) {
plugin.getLogger().warning("source.mode '" + raw + "' is not player or global - using player.");
huntMode = HuntMode.PLAYER;
}
}

// ====================================
// How far from the character a source is placed, and when it is placed again
// ====================================
Expand Down Expand Up @@ -200,7 +214,7 @@ private void loadSpawnFilters() {
}

private void loadDetectionSettings() {
maxDetectionDistance = plugin.getConfig().getDouble("detection.max-detection-distance", 1000.0);
maxDetectionDistance = plugin.getConfig().getDouble("detection.max-detection-distance", 2500.0);
pinpointRange = plugin.getConfig().getDouble("detection.pinpoint-range", 12.0);
closeRangeThreshold = plugin.getConfig().getDouble("detection.close-range-threshold", 200.0);
threeRingsDistance = plugin.getConfig().getDouble("detection.ring-thresholds.three-rings", 100.0);
Expand Down Expand Up @@ -433,6 +447,7 @@ private ItemReward parseItemReward(String itemString) {
public double getMaxX() { return maxX; }
public double getMinZ() { return minZ; }
public double getMaxZ() { return maxZ; }
public HuntMode getHuntMode() { return huntMode; }
public double getMinSourceDistance() { return minSourceDistance; }
public double getMaxSourceDistance() { return maxSourceDistance; }
public double getRerollDistance() { return rerollDistance; }
Expand Down Expand Up @@ -469,6 +484,29 @@ private ItemReward parseItemReward(String itemString) {


// =========== Rules for valid source spawn locations ================
// ====================================
// player: each character hunts their own source near them.
// global: one source anywhere in the area, shared by everyone.
// ====================================
public enum HuntMode {
PLAYER, GLOBAL;

// As written in config.yml and typed in /geiger mode
public String key() {
return name().toLowerCase(Locale.ROOT);
}

// The mode named by <raw>, or null if none
public static HuntMode parse(String raw) {
for (HuntMode mode : values()) {
if (mode.key().equalsIgnoreCase(raw.trim())) {
return mode;
}
}
return null;
}
}

public static class SpawnFilterConfig {
private int maxAttempts = 50;
private boolean rejectLiquid = true;
Expand Down
Loading