From 6ed9634e4190b7b4396e6dd9e5697a7e87800f19 Mon Sep 17 00:00:00 2001
From: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
Date: Fri, 25 Sep 2026 13:10:31 +0000
Subject: [PATCH 1/2] Treat a character's healing injury with Victorian
surgery.
Random diagnoses never matched the injuries players actually had, so a physician now operates on the real ailment and a success removes it.
---
README.md | 14 +-
pom.xml | 26 +
.../tfminecraft/surgery/SurgeryPlugin.java | 4 +-
.../surgery/commands/SurgeryCommand.java | 191 +++++-
.../surgery/listeners/PlayerListener.java | 3 +
.../surgery/managers/DiagnosisChecker.java | 42 --
.../managers/SurgeryCompletionHandler.java | 165 +++--
.../surgery/managers/SurgeryConstants.java | 20 +-
.../surgery/managers/SurgeryItemHandler.java | 646 ++++++++----------
.../surgery/managers/SurgeryItemsConfig.java | 57 +-
.../managers/SurgeryMechanicsManager.java | 399 ++++-------
.../surgery/managers/SurgeryMenuBuilder.java | 46 +-
.../surgery/managers/SurgeryMenuManager.java | 21 +-
.../managers/SurgeryRequestManager.java | 37 +
.../surgery/managers/SurgeryStateManager.java | 33 +-
.../surgery/managers/SurgeryTool.java | 49 ++
.../surgery/managers/SurgeryUIUpdater.java | 53 +-
.../surgery/procedures/Ailments.java | 44 ++
.../surgery/procedures/Complication.java | 27 +
.../surgery/procedures/Durations.java | 51 ++
.../surgery/procedures/Procedure.java | 22 +
.../surgery/procedures/ProcedureRegistry.java | 71 ++
src/main/resources/config.yml | 198 ++----
src/main/resources/messages.yml | 353 +++++-----
src/main/resources/plugin.yml | 12 +-
src/main/resources/surgeryItemsConfig.yml | 24 +-
.../surgery/procedures/DurationsTest.java | 35 +
.../procedures/ProcedureRegistryTest.java | 85 +++
28 files changed, 1480 insertions(+), 1248 deletions(-)
delete mode 100644 src/main/java/net/tfminecraft/surgery/managers/DiagnosisChecker.java
create mode 100644 src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java
create mode 100644 src/main/java/net/tfminecraft/surgery/managers/SurgeryTool.java
create mode 100644 src/main/java/net/tfminecraft/surgery/procedures/Ailments.java
create mode 100644 src/main/java/net/tfminecraft/surgery/procedures/Complication.java
create mode 100644 src/main/java/net/tfminecraft/surgery/procedures/Durations.java
create mode 100644 src/main/java/net/tfminecraft/surgery/procedures/Procedure.java
create mode 100644 src/main/java/net/tfminecraft/surgery/procedures/ProcedureRegistry.java
create mode 100644 src/test/java/net/tfminecraft/surgery/procedures/DurationsTest.java
create mode 100644 src/test/java/net/tfminecraft/surgery/procedures/ProcedureRegistryTest.java
diff --git a/README.md b/README.md
index b8f8fe2..7995eda 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,18 @@
> A hands-on medical roleplay minigame for TF-Minecraft.
-Surgery puts a player's treatment into another player's hands. A nearby patient becomes the subject of an interactive operating-table menu, where the surgeon diagnoses the problem, chooses tools, and manages the patient's changing condition.
+Surgery lets a physician mend another character's injuries on the operating table. It treats the healing ailments from [RPCharacters](https://github.com/TF-Minecraft/RPCharacters), such as a broken arm, a broken leg, or a half-blind eye, using Victorian medicine.
-Each move matters: opening an incision, cleaning the site, treating a complication, or closing the wound can change what the surgeon needs to do next.
+Each move matters: sedating the patient, cleaning the site, opening an incision, setting a bone, or closing the wound can change what the surgeon needs to do next.
## Features
-- **Interactive operations** — an inventory menu brings surgical tools and patient status together in one view.
-- **Hidden diagnoses** — ultrasound reveals ailments ranging from broken bones and infections to fantastical conditions.
-- **Purposeful instruments** — tools include scalpels, anesthetic, sponges, stitches, transfusions, and a defibrillator.
+- **Real ailments** — the operation targets the patient's own healing injury. A success cures it; a failure makes it take longer to heal. Permanent injuries cannot be operated on.
+- **Physicians only** — operating requires the Physician profession upgrade, the same permission that unlocks crafting the instruments.
+- **Patient consent** — the patient must accept the surgeon's offer, which states the ailment and what a failure would cost.
+- **Victorian instruments** — stethoscope, chloroform, carbolic acid, scalpel, catgut suture, artery forceps, splint, silver wire, smelling salts, transfusion syringe, clinical thermometer, willow-bark tincture, and linen dressing.
- **Changing patient vitals** — consciousness, pulse, temperature, bleeding, and cleanliness affect the course of treatment.
-- **Diagnosis-specific challenges** — different ailments require different interventions, including bone repairs and temperature management.
-- **Consequences for each move** — failed actions and worsening complications create pressure to finish treatment before the operation fails.
+- **Procedures and complications** — each ailment has its own procedure, with bones to set and complications such as haemorrhage, shock, and sepsis.
Originally created by [Justinas Launikonis](https://github.com/JustinasLa).
diff --git a/pom.xml b/pom.xml
index a2ad49d..2884d0a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,7 @@
21
UTF-8
+ 2.4.0
2.0.0
@@ -39,6 +40,26 @@
+
+
+ net.tfminecraft
+ rpcharacters
+ ${rpcharacters.version}
+ provided
+
+
+ *
+ *
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.2
+ test
+
@@ -71,6 +92,11 @@
maven-compiler-plugin
3.14.1
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
diff --git a/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java b/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java
index 56c7abe..86a8615 100644
--- a/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java
+++ b/src/main/java/net/tfminecraft/surgery/SurgeryPlugin.java
@@ -32,7 +32,9 @@ public void onEnable() {
}
getServer().getPluginManager().registerEvents(new PlayerListener(surgeryMenuManager), this);
- getCommand("surgery").setExecutor(new SurgeryCommand(surgeryMenuManager, this));
+ SurgeryCommand surgeryCommand = new SurgeryCommand(surgeryMenuManager, this);
+ getCommand("surgery").setExecutor(surgeryCommand);
+ getCommand("surgery").setTabCompleter(surgeryCommand);
getLogger().info("surgery has been enabled!");
}
diff --git a/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java b/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java
index f1dfada..566247b 100644
--- a/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java
+++ b/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java
@@ -1,60 +1,173 @@
package net.tfminecraft.surgery.commands;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.tfminecraft.rpcharacters.api.HealingInjuries.HealingInjury;
+import net.tfminecraft.surgery.SurgeryPlugin;
+import net.tfminecraft.surgery.managers.SurgeryMenuManager;
+import net.tfminecraft.surgery.managers.SurgeryRequestManager;
+import net.tfminecraft.surgery.managers.SurgeryUIUpdater;
+import net.tfminecraft.surgery.procedures.Ailments;
+import net.tfminecraft.surgery.procedures.Durations;
import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
-import net.tfminecraft.surgery.managers.SurgeryMenuManager;
-import net.tfminecraft.surgery.managers.SurgeryUIUpdater;
-import net.tfminecraft.surgery.SurgeryPlugin;
-public class SurgeryCommand implements CommandExecutor {
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class SurgeryCommand implements TabExecutor {
private final SurgeryMenuManager menuManager;
private final SurgeryPlugin plugin;
private final SurgeryUIUpdater uiUpdater;
+ private final SurgeryRequestManager requests;
public SurgeryCommand(SurgeryMenuManager menuManager, SurgeryPlugin plugin) {
this.menuManager = menuManager;
this.plugin = plugin;
this.uiUpdater = menuManager.getUiUpdater();
+ this.requests = menuManager.getRequestManager();
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- if (!(sender instanceof Player)) {
+ if (!(sender instanceof Player player)) {
sender.sendMessage(uiUpdater.getMessage("command-console", "&cThis command can only be used by players."));
return true;
}
- Player surgeon = (Player) sender;
-
- // Block re-entry: opening a second menu fires the old menu's close event,
- // which fails the surgery and wipes the freshly initialized session
- if (menuManager.getStateManager().hasSession(surgeon.getUniqueId())) {
- surgeon.sendMessage(uiUpdater.getMessage("command-already-in-surgery",
- "&cYou are already performing surgery! Close the current menu first."));
+ if (args.length == 0) {
+ player.sendMessage(uiUpdater.getMessage("command-usage", "&cUsage: /surgery | accept | deny"));
return true;
}
- // Check if player name is provided in command
- if (args.length == 0) {
- surgeon.sendMessage(uiUpdater.getMessage("command-usage", "&cUsage: /surgery "));
- return true;
+ String sub = args[0].toLowerCase(Locale.ROOT);
+ if (sub.equals("accept")) {
+ accept(player);
+ } else if (sub.equals("deny")) {
+ deny(player);
+ } else {
+ offer(player, args[0]);
+ }
+ return true;
+ }
+
+ // ==============================================
+ // Surgeon offers to operate on a patient's worst healing ailment
+ // ==============================================
+ private void offer(Player surgeon, String patientName) {
+ if (!canOperate(surgeon)) {
+ surgeon.sendMessage(uiUpdater.getMessage("command-no-permission"));
+ return;
}
- // Get the target player (patient)
- Player patient = Bukkit.getPlayer(args[0]);
+ Player patient = Bukkit.getPlayerExact(patientName);
if (patient == null || !patient.isOnline()) {
surgeon.sendMessage(uiUpdater.getMessage("command-player-not-found", "&cPlayer not found or not online!"));
- return true;
+ return;
+ }
+ if (!checkCanStart(surgeon, patient)) {
+ return;
+ }
+
+ HealingInjury ailment = Ailments.mostSevere(patient);
+ if (ailment == null) {
+ surgeon.sendMessage(uiUpdater.getMessage("command-no-ailment").replace("%patient%", patient.getName()));
+ return;
+ }
+
+ int timeoutSeconds = Math.max(10, plugin.getConfig().getInt("request-timeout-seconds", 60));
+ requests.offer(patient.getUniqueId(), surgeon.getUniqueId(), ailment.traitId(),
+ System.currentTimeMillis() + timeoutSeconds * 1000L);
+
+ String ailmentName = ChatColor.stripColor(ailment.displayName());
+ String penalty = plugin.getConfig().getString("failure-healing-penalty", "12h");
+ long penaltyMs = Durations.parseMs(penalty);
+ surgeon.sendMessage(uiUpdater.getMessage("request-sent")
+ .replace("%patient%", patient.getName()).replace("%ailment%", ailmentName));
+ patient.sendMessage(uiUpdater.getMessage("request-received")
+ .replace("%surgeon%", surgeon.getName())
+ .replace("%ailment%", ailmentName)
+ .replace("%remaining%", Durations.formatHours(ailment.remainingMs()))
+ .replace("%extra%", penaltyMs > 0 ? Durations.formatHours(penaltyMs) : penalty)
+ .replace("%seconds%", String.valueOf(timeoutSeconds)));
+ patient.sendMessage(LegacyComponentSerializer.legacySection()
+ .deserialize(uiUpdater.getMessage("request-accept-button"))
+ .clickEvent(ClickEvent.runCommand("/surgery accept"))
+ .append(LegacyComponentSerializer.legacySection().deserialize(" "))
+ .append(LegacyComponentSerializer.legacySection()
+ .deserialize(uiUpdater.getMessage("request-deny-button"))
+ .clickEvent(ClickEvent.runCommand("/surgery deny"))));
+ }
+
+ // ==============================================
+ // Patient agrees; the surgeon's operating menu opens
+ // ==============================================
+ private void accept(Player patient) {
+ SurgeryRequestManager.Request request = requests.take(patient.getUniqueId(), System.currentTimeMillis());
+ if (request == null) {
+ patient.sendMessage(uiUpdater.getMessage("request-none"));
+ return;
+ }
+ Player surgeon = Bukkit.getPlayer(request.surgeonId());
+ if (surgeon == null || !surgeon.isOnline() || !canOperate(surgeon)) {
+ patient.sendMessage(uiUpdater.getMessage("request-surgeon-gone"));
+ return;
+ }
+ if (!checkCanStart(surgeon, patient)) {
+ patient.sendMessage(uiUpdater.getMessage("request-cannot-start").replace("%surgeon%", surgeon.getName()));
+ return;
+ }
+ HealingInjury ailment = Ailments.find(patient, request.traitId());
+ if (ailment == null) {
+ patient.sendMessage(uiUpdater.getMessage("request-ailment-gone"));
+ return;
+ }
+
+ patient.sendMessage(uiUpdater.getMessage("request-accepted-patient").replace("%surgeon%", surgeon.getName()));
+ surgeon.sendMessage(uiUpdater.getMessage("request-accepted-surgeon").replace("%patient%", patient.getName()));
+ menuManager.openSurgeryMenu(surgeon, patient,
+ new HealingInjury(ailment.traitId(), ChatColor.stripColor(ailment.displayName()), ailment.remainingMs()));
+ }
+
+ private void deny(Player patient) {
+ SurgeryRequestManager.Request request = requests.take(patient.getUniqueId(), System.currentTimeMillis());
+ if (request == null) {
+ patient.sendMessage(uiUpdater.getMessage("request-none"));
+ return;
+ }
+ patient.sendMessage(uiUpdater.getMessage("request-denied-patient"));
+ Player surgeon = Bukkit.getPlayer(request.surgeonId());
+ if (surgeon != null) {
+ surgeon.sendMessage(uiUpdater.getMessage("request-denied-surgeon").replace("%patient%", patient.getName()));
+ }
+ }
+
+ private boolean canOperate(Player surgeon) {
+ String permission = plugin.getConfig().getString("permission", "professions.physician");
+ return permission == null || permission.isBlank() || surgeon.hasPermission(permission);
+ }
+
+ // ==============================================
+ // Checks shared by offering and accepting; messages the surgeon on failure
+ // ==============================================
+ private boolean checkCanStart(Player surgeon, Player patient) {
+ // Block re-entry: opening a second menu fires the old menu's close event,
+ // which fails the surgery and wipes the freshly initialized session
+ if (menuManager.getStateManager().hasSession(surgeon.getUniqueId())) {
+ surgeon.sendMessage(uiUpdater.getMessage("command-already-in-surgery",
+ "&cYou are already performing surgery! Close the current menu first."));
+ return false;
}
- // Check if trying to "operate" on self
if (patient.getUniqueId().equals(surgeon.getUniqueId())) {
surgeon.sendMessage(uiUpdater.getMessage("command-self-surgery", "&cYou cannot perform surgery on yourself!"));
- return true;
+ return false;
}
// One surgery per patient: a second surgeon opening a menu on the same
@@ -62,17 +175,16 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
if (menuManager.getStateManager().isPatientInSurgery(patient.getUniqueId())) {
surgeon.sendMessage(uiUpdater.getMessage("command-patient-in-surgery",
"&cThat player is already undergoing surgery!"));
- return true;
+ return false;
}
// A player mid-operation as a surgeon cannot also be a patient
if (menuManager.getStateManager().hasSession(patient.getUniqueId())) {
surgeon.sendMessage(uiUpdater.getMessage("command-patient-is-operating",
"&cThat player is busy performing surgery themselves!"));
- return true;
+ return false;
}
- // Check if patient is within config distance
// World check first: distance() throws across worlds
double maxDistance = plugin.getConfig().getDouble("max-surgery-distance", 5.0);
if (!surgeon.getWorld().equals(patient.getWorld())
@@ -82,12 +194,31 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
? String.valueOf((long) maxDistance) : String.valueOf(maxDistance);
surgeon.sendMessage(uiUpdater.getMessage("command-too-far", "&cThe patient must be within %distance% blocks of you!")
.replace("%distance%", distance));
- return true;
+ return false;
}
-
- // Open surgery menu for the player, operating on the specified "patient"
- menuManager.openSurgeryMenu(surgeon, patient);
-
return true;
}
+
+ @Override
+ public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+ List options = new ArrayList<>();
+ if (args.length != 1) {
+ return options;
+ }
+ String prefix = args[0].toLowerCase(Locale.ROOT);
+ for (String sub : List.of("accept", "deny")) {
+ if (sub.startsWith(prefix)) {
+ options.add(sub);
+ }
+ }
+ if (sender instanceof Player player && canOperate(player)) {
+ for (Player online : Bukkit.getOnlinePlayers()) {
+ if (!online.equals(player) && player.canSee(online)
+ && online.getName().toLowerCase(Locale.ROOT).startsWith(prefix)) {
+ options.add(online.getName());
+ }
+ }
+ }
+ return options;
+ }
}
diff --git a/src/main/java/net/tfminecraft/surgery/listeners/PlayerListener.java b/src/main/java/net/tfminecraft/surgery/listeners/PlayerListener.java
index a7702b7..88bc21b 100644
--- a/src/main/java/net/tfminecraft/surgery/listeners/PlayerListener.java
+++ b/src/main/java/net/tfminecraft/surgery/listeners/PlayerListener.java
@@ -27,6 +27,9 @@ public void onPlayerQuit(PlayerQuitEvent event) {
// The quitting player may also be someone's patient; fail that surgeon's
// surgery now instead of on their next move
menuManager.handlePatientQuit(event.getPlayer());
+
+ // Pending offers to or from the quitting player can no longer be answered
+ menuManager.getRequestManager().forget(event.getPlayer().getUniqueId());
}
@EventHandler
diff --git a/src/main/java/net/tfminecraft/surgery/managers/DiagnosisChecker.java b/src/main/java/net/tfminecraft/surgery/managers/DiagnosisChecker.java
deleted file mode 100644
index 3aeaf93..0000000
--- a/src/main/java/net/tfminecraft/surgery/managers/DiagnosisChecker.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.tfminecraft.surgery.managers;
-
-import org.bukkit.plugin.java.JavaPlugin;
-
-import java.util.List;
-
-// ==============================================
-// Helper class to check diagnosis categories from config
-// ==============================================
-public class DiagnosisChecker {
-
- private final JavaPlugin plugin;
- private List fluDiagnoses;
-
- public DiagnosisChecker(JavaPlugin plugin) {
- this.plugin = plugin;
- loadConfig();
- }
-
- // ==============================================
- // Loads configurable diagnosis categories
- // ==============================================
- public void loadConfig() {
- fluDiagnoses = plugin.getConfig().getStringList("flu-diagnoses");
- }
-
- // ==============================================
- // Checks if a diagnosis should have bones
- // A diagnosis has bones if it's defined in the bone-counts section
- // ==============================================
- public boolean hasBones(String diagnosis) {
- return plugin.getConfig().contains("bone-counts." + diagnosis);
- }
-
- // ==============================================
- // Checks if a diagnosis is a flu type
- // Flu types are defined in the flu-diagnoses config list
- // ==============================================
- public boolean isFlu(String diagnosis) {
- return fluDiagnoses != null && fluDiagnoses.contains(diagnosis);
- }
-}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
index d9f3bbf..e8a615e 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
@@ -1,7 +1,8 @@
package net.tfminecraft.surgery.managers;
+import net.tfminecraft.surgery.procedures.Ailments;
+import net.tfminecraft.surgery.procedures.Durations;
import org.bukkit.Bukkit;
-import org.bukkit.ChatColor;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@@ -10,86 +11,137 @@
// ==============================================
// Handles surgery completion (success and failure)
+// Success cures the patient's healing ailment in RPCharacters; failure makes
+// it take longer to heal
// ==============================================
public class SurgeryCompletionHandler {
-
+
+ private static final long DEFAULT_FAILURE_PENALTY_MS = 12 * 3_600_000L;
+
private final JavaPlugin plugin;
private final SurgeryStateManager stateManager;
private final SurgeryUIUpdater uiUpdater;
-
+
public SurgeryCompletionHandler(JavaPlugin plugin, SurgeryStateManager stateManager, SurgeryUIUpdater uiUpdater) {
this.plugin = plugin;
this.stateManager = stateManager;
this.uiUpdater = uiUpdater;
}
-
+
// ==============================================
// Checks if all success conditions are met
// ==============================================
public boolean isSurgerySuccessful(UUID playerId) {
- // 1. Check if diagnosis is cured
+ // 1. The ailment has been treated and dressed
if (!stateManager.isCured(playerId)) { return false; }
-
+
// 2. Pulse must be "Strong" (LIME)
if (!stateManager.getPulse(playerId).equals("Strong")) { return false; }
-
+
// 3. Status must be "Unconscious" (LIME)
if (!stateManager.getStatus(playerId).equals("Unconscious")) { return false; }
-
+
// 4. Temperature must be at or below the configured success threshold (LIME)
double successThreshold = plugin.getConfig().getDouble("temperature.success-threshold", 100.0);
if (stateManager.getTemperature(playerId) > successThreshold) { return false; }
-
+
// 5. Operation site must be "Clean" (LIME)
if (!stateManager.getOperationSite(playerId).equals("Clean")) { return false; }
-
+
// 6. Incisions must be 0 (LIME)
if (stateManager.getIncisions(playerId) != 0) { return false; }
-
- // 7. All bones must be fixed
+
+ // 7. All bones must be set
if (stateManager.getBrokenBones(playerId) != 0 || stateManager.getShatteredBones(playerId) != 0) { return false; }
-
+
// 8. No bleeding
if (stateManager.isBleeding(playerId)) { return false; }
-
+
return true;
}
-
+
// ==============================================
// Handles successful surgery completion
// ==============================================
- public void handleSuccess(Player player) {
- UUID playerId = player.getUniqueId();
-
- executeCompletionCommand(player, true);
-
- stateManager.cleanup(playerId);
+ public void handleSuccess(Player surgeon) {
+ UUID surgeonId = surgeon.getUniqueId();
+ String traitId = stateManager.getTraitId(surgeonId);
+ String ailment = stateManager.getAilmentName(surgeonId);
+ Player patient = patientOf(surgeonId);
+
+ executeCompletionCommand(surgeon, true);
+ stateManager.cleanup(surgeonId);
+ closeMenuNextTick(surgeon);
- closeMenuNextTick(player);
- player.playSound(player.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f);
- player.sendMessage(uiUpdater.getMessage("surgery-successful"));
- player.sendMessage(uiUpdater.getMessage("surgery-successful-subtitle"));
+ // The ailment may have healed on its own while the operation was running
+ if (patient == null || !Ailments.cure(patient, traitId)) {
+ surgeon.sendMessage(uiUpdater.getMessage("ailment-already-healed").replace("%ailment%", ailment));
+ return;
+ }
+
+ surgeon.playSound(surgeon.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f);
+ surgeon.sendMessage(uiUpdater.getMessage("surgery-successful"));
+ surgeon.sendMessage(uiUpdater.getMessage("surgery-successful-subtitle")
+ .replace("%ailment%", ailment).replace("%patient%", patient.getName()));
+ patient.sendMessage(uiUpdater.getMessage("patient-surgery-successful")
+ .replace("%ailment%", ailment).replace("%surgeon%", surgeon.getName()));
}
-
+
// ==============================================
// Fails the surgery with a message
// ==============================================
- public void failSurgery(Player player, String message) {
- UUID playerId = player.getUniqueId();
-
+ public void failSurgery(Player surgeon, String message) {
+ UUID surgeonId = surgeon.getUniqueId();
+
// Check if player is actually in surgery (prevent duplicate messages)
- if (!stateManager.hasSession(playerId)) { return; }
-
- executeCompletionCommand(player, false);
-
- stateManager.cleanup(playerId);
+ if (!stateManager.hasSession(surgeonId)) { return; }
+
+ executeCompletionCommand(surgeon, false);
+ // Only a patient who was actually sedated or cut is left worse off
+ String extra = stateManager.hasOperated(surgeonId) ? lengthenHealing(surgeon) : null;
- closeMenuNextTick(player);
- player.playSound(player.getLocation(), Sound.ENTITY_WITHER_DEATH, 1.0f, 0.8f);
- player.sendMessage(uiUpdater.getMessage("surgery-failed"));
- player.sendMessage(message);
+ stateManager.cleanup(surgeonId);
+
+ closeMenuNextTick(surgeon);
+ surgeon.playSound(surgeon.getLocation(), Sound.ENTITY_WITHER_DEATH, 1.0f, 0.8f);
+ surgeon.sendMessage(uiUpdater.getMessage("surgery-failed"));
+ surgeon.sendMessage(message);
+ if (extra != null) {
+ surgeon.sendMessage(uiUpdater.getMessage("surgery-failed-penalty").replace("%extra%", extra));
+ }
+ }
+
+ // ==============================================
+ // Adds the failure penalty to the patient's healing time
+ // Returns the added time for messages, or null if nothing was added
+ // ==============================================
+ private String lengthenHealing(Player surgeon) {
+ UUID surgeonId = surgeon.getUniqueId();
+ Player patient = patientOf(surgeonId);
+ String traitId = stateManager.getTraitId(surgeonId);
+ if (patient == null || traitId == null) {
+ return null;
+ }
+ long penalty = Durations.parseMs(plugin.getConfig().getString("failure-healing-penalty", "12h"));
+ if (penalty < 0L) {
+ plugin.getLogger().warning("Invalid failure-healing-penalty, using 12h");
+ penalty = DEFAULT_FAILURE_PENALTY_MS;
+ }
+ if (penalty == 0L) {
+ return null;
+ }
+ long remaining = Ailments.extend(patient, traitId, penalty);
+ if (remaining < 0L) {
+ return null;
+ }
+ String extra = Durations.formatHours(penalty);
+ patient.sendMessage(uiUpdater.getMessage("patient-surgery-failed")
+ .replace("%ailment%", stateManager.getAilmentName(surgeonId))
+ .replace("%extra%", extra)
+ .replace("%remaining%", Durations.formatHours(remaining)));
+ return extra;
}
-
+
// ==============================================
// Closes the menu on the next tick; calling closeInventory directly inside
// an InventoryClickEvent handler is undefined behavior per the Bukkit docs
@@ -99,49 +151,56 @@ private void closeMenuNextTick(Player player) {
}
// ==============================================
- // Handles player giving up on surgery (only if they didn't complete it successfully)
+ // Handles the surgeon closing the menu before finishing
+ // Stopping before the patient is sedated or cut does no harm; after that, the surgery fails
// ==============================================
public void handleAbandonment(Player player) {
UUID playerId = player.getUniqueId();
-
- // Only fail if player still has active surgery data (not cleaned up from success)
- if (stateManager.hasDiagnosis(playerId)) {
+ if (!stateManager.hasSession(playerId)) {
+ return;
+ }
+ if (stateManager.hasOperated(playerId)) {
failSurgery(player, uiUpdater.getMessage("failure-gave-up"));
} else {
- // No diagnosis yet (menu closed before ultrasound) - nothing to fail,
- // but state initialized on menu open must still be released
stateManager.cleanup(playerId);
+ player.sendMessage(uiUpdater.getMessage("surgery-cancelled"));
}
}
-
+
// ==============================================
// Handles the surgeon disconnecting mid-surgery
- // Runs the failure command (no free escape by logging out) but skips the
- // messages, sounds, and menu close, which are pointless for a quitting
- // player. If the close event already failed the surgery, the session is
- // gone and this only cleans up.
+ // Runs the failure command and penalty once treatment has begun (no
+ // free escape by logging out) but skips the surgeon's messages, sounds, and
+ // menu close, which are pointless for a quitting player
// ==============================================
public void handleQuit(Player player) {
UUID playerId = player.getUniqueId();
- if (stateManager.hasDiagnosis(playerId)) {
+ if (stateManager.hasOperated(playerId)) {
executeCompletionCommand(player, false);
+ lengthenHealing(player);
}
stateManager.cleanup(playerId);
}
+ private Player patientOf(UUID surgeonId) {
+ UUID patientId = stateManager.getPatientUuid(surgeonId);
+ return patientId == null ? null : Bukkit.getPlayer(patientId);
+ }
+
// ==============================================
// Executes the configured command for surgery completion (success or failure)
// ==============================================
private void executeCompletionCommand(Player surgeon, boolean success) {
UUID surgeonId = surgeon.getUniqueId();
String patientName = stateManager.getPatientName(surgeonId);
-
+
String configKey = success ? "commands.surgery-success" : "commands.surgery-failure";
String command = plugin.getConfig().getString(configKey, "");
-
+
if (command != null && !command.isEmpty()) {
command = command.replace("%surgeon%", surgeon.getName());
command = command.replace("%player%", patientName);
+ command = command.replace("%ailment%", stateManager.getAilmentName(surgeonId));
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);
}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryConstants.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryConstants.java
index 1994aa0..58fdafa 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryConstants.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryConstants.java
@@ -22,27 +22,13 @@ public class SurgeryConstants {
public static final int INFO_SLOT_FIRST = SLOT_DIAGNOSIS;
public static final int INFO_SLOT_LAST = SLOT_SKILL_FAIL;
- // Tool slots
- public static final int SLOT_SPONGE = 28;
- public static final int SLOT_SCALPEL = 29;
- public static final int SLOT_STITCHES = 30;
- public static final int SLOT_ANTIBIOTICS = 31;
- public static final int SLOT_ANTISEPTIC = 32;
- public static final int SLOT_SURGICAL_GLOVE = 33;
- public static final int SLOT_ULTRASOUND = 34;
- public static final int SLOT_LAB_KIT = 37;
- public static final int SLOT_ANESTHETIC = 38;
- public static final int SLOT_DEFIBRILLATOR = 39;
- public static final int SLOT_PINS = 40;
- public static final int SLOT_SPLINT = 41;
- public static final int SLOT_CLAMP = 42;
- public static final int SLOT_TRANSFUSION = 43;
+ // Tool slots live on SurgeryTool
// List of possible patient statuses (with corresponding colors)
public static final String[] PATIENT_STATUSES = {
"Awake", // YELLOW_CONCRETE
"Unconscious", // LIME_CONCRETE
- "Heart Stopped", // RED_CONCRETE
+ "Collapsed", // RED_CONCRETE
"Coming to" // ORANGE_CONCRETE
};
@@ -66,7 +52,7 @@ public static Material getStatusColor(String status) {
return switch (status) {
case "Awake" -> Material.YELLOW_CONCRETE;
case "Unconscious" -> Material.LIME_CONCRETE;
- case "Heart Stopped" -> Material.RED_CONCRETE;
+ case "Collapsed" -> Material.RED_CONCRETE;
case "Coming to" -> Material.ORANGE_CONCRETE;
default -> Material.GRAY_CONCRETE;
};
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemHandler.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemHandler.java
index b6641ea..602ea90 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemHandler.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemHandler.java
@@ -1,5 +1,7 @@
package net.tfminecraft.surgery.managers;
+import net.tfminecraft.surgery.procedures.Complication;
+import net.tfminecraft.surgery.procedures.Procedure;
import net.tfminecraft.tlibs.objects.api.ItemAPI;
import org.bukkit.ChatColor;
import org.bukkit.Material;
@@ -9,6 +11,9 @@
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@@ -16,46 +21,27 @@
// Handles all surgery item clicks and tool logic
// ==============================================
public class SurgeryItemHandler {
-
+
private final JavaPlugin plugin;
private final ItemAPI api;
private final SurgeryStateManager stateManager;
private final SurgeryUIUpdater uiUpdater;
private final SurgeryMechanicsManager mechanicsManager;
private final SurgeryCompletionHandler completionHandler;
- private final DiagnosisChecker diagnosisChecker;
private final SurgeryItemsConfig itemsConfig;
- // Skill fail message lists
- private java.util.List skillFailLabKit;
- private java.util.List skillFailUltrasound;
- private java.util.List skillFailScalpel;
- private java.util.List skillFailStitches;
- private java.util.List skillFailAntibiotics;
- private java.util.List skillFailTransfusion;
- private java.util.List skillFailAntiseptic;
- private java.util.List skillFailSponge;
- private java.util.List skillFailAnesthetic;
- private java.util.List skillFailDefibrillator;
- private java.util.List skillFailSplint;
- private java.util.List skillFailPins;
- private java.util.List skillFailClamp;
- private java.util.List skillFailSurgicalGlove;
-
- // Diagnoses list
- private java.util.List diagnosesList;
-
- public SurgeryItemHandler(JavaPlugin plugin, ItemAPI api, SurgeryStateManager stateManager,
+ // Skill fail message lists, one per tool
+ private final Map> skillFailMessages = new EnumMap<>(SurgeryTool.class);
+
+ public SurgeryItemHandler(JavaPlugin plugin, ItemAPI api, SurgeryStateManager stateManager,
SurgeryUIUpdater uiUpdater, SurgeryMechanicsManager mechanicsManager,
- SurgeryCompletionHandler completionHandler, DiagnosisChecker diagnosisChecker,
- SurgeryItemsConfig itemsConfig) {
+ SurgeryCompletionHandler completionHandler, SurgeryItemsConfig itemsConfig) {
this.plugin = plugin;
this.api = api;
this.stateManager = stateManager;
this.uiUpdater = uiUpdater;
this.mechanicsManager = mechanicsManager;
this.completionHandler = completionHandler;
- this.diagnosisChecker = diagnosisChecker;
this.itemsConfig = itemsConfig;
}
@@ -63,24 +49,11 @@ public SurgeryItemHandler(JavaPlugin plugin, ItemAPI api, SurgeryStateManager st
// Initializes skill fail messages from messages.yml
// ==============================================
public void initialize() {
- skillFailLabKit = uiUpdater.getMessageList("skill-fail-lab-kit");
- skillFailUltrasound = uiUpdater.getMessageList("skill-fail-ultrasound");
- skillFailScalpel = uiUpdater.getMessageList("skill-fail-scalpel");
- skillFailStitches = uiUpdater.getMessageList("skill-fail-stitches");
- skillFailAntibiotics = uiUpdater.getMessageList("skill-fail-antibiotics");
- skillFailTransfusion = uiUpdater.getMessageList("skill-fail-transfusion");
- skillFailAntiseptic = uiUpdater.getMessageList("skill-fail-antiseptic");
- skillFailSponge = uiUpdater.getMessageList("skill-fail-sponge");
- skillFailAnesthetic = uiUpdater.getMessageList("skill-fail-anesthetic");
- skillFailDefibrillator = uiUpdater.getMessageList("skill-fail-defibrillator");
- skillFailSplint = uiUpdater.getMessageList("skill-fail-splint");
- skillFailPins = uiUpdater.getMessageList("skill-fail-pins");
- skillFailClamp = uiUpdater.getMessageList("skill-fail-clamp");
- skillFailSurgicalGlove = uiUpdater.getMessageList("skill-fail-surgical-glove");
-
- diagnosesList = plugin.getConfig().getStringList("diagnoses");
+ for (SurgeryTool tool : SurgeryTool.values()) {
+ skillFailMessages.put(tool, uiUpdater.getMessageList("skill-fail-" + tool.getConfigKey()));
+ }
}
-
+
// ==============================================
// Handles clicking on a surgery menu item
// ==============================================
@@ -90,27 +63,26 @@ public void handleItemClick(Player player, ItemStack clickedItem, int slot) {
if (clickedItem == null || clickedItem.getType().isAir()) {
return;
}
-
- UUID playerId = player.getUniqueId();
-
- // Ignore clicks on info blocks
- if (slot >= SurgeryConstants.INFO_SLOT_FIRST && slot <= SurgeryConstants.INFO_SLOT_LAST) {
+
+ // Ignore clicks on info blocks and anything that is not a tool
+ SurgeryTool tool = SurgeryTool.fromSlot(slot);
+ if (tool == null) {
return;
}
+ UUID playerId = player.getUniqueId();
+
// Special check for scalpel - cannot be used when patient is awake
- if (slot == SurgeryConstants.SLOT_SCALPEL) {
- String patientStatus = stateManager.getStatus(playerId);
- if (patientStatus.equals("Awake")) {
- completionHandler.failSurgery(player, uiUpdater.getMessage("failure-stabbed-awake"));
- return;
- }
+ if (tool == SurgeryTool.SCALPEL && stateManager.getStatus(playerId).equals("Awake")) {
+ stateManager.setOperated(playerId, true);
+ completionHandler.failSurgery(player, uiUpdater.getMessage("failure-stabbed-awake"));
+ return;
}
-
+
// Check if the player has this item in their inventory and remove it
- if (removeItemFromPlayer(player, clickedItem)) {
+ if (removeItemFromPlayer(player, tool)) {
// Process per-move effects before updating menu
- mechanicsManager.processMoveEffects(player, slot);
+ mechanicsManager.processMoveEffects(player, tool);
// Move effects may have ended the surgery (death timers, fever, bleed-out);
// the session is cleaned up and the menu is closed, so stop here
@@ -119,8 +91,8 @@ public void handleItemClick(Player player, ItemStack clickedItem, int slot) {
}
// Update the menu based on what was clicked
- updateMenu(player, slot);
-
+ updateMenu(player, tool);
+
// Play correct sound only if no skill fail occurred
String skillFailMsg = stateManager.getSkillFail(playerId);
if (skillFailMsg.isEmpty()) {
@@ -128,49 +100,47 @@ public void handleItemClick(Player player, ItemStack clickedItem, int slot) {
}
} else {
// Player doesn't have the item - show error message
- player.sendMessage(ChatColor.translateAlternateColorCodes('&',
- uiUpdater.getMessage("item-not-in-inventory")));
+ player.sendMessage(uiUpdater.getMessage("item-not-in-inventory"));
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
}
}
-
+
// ==============================================
// Updates the menu when specific items are clicked
// ==============================================
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
- private void updateMenu(Player player, int clickedSlot) {
+ private void updateMenu(Player player, SurgeryTool tool) {
Inventory menu = player.getOpenInventory().getTopInventory();
UUID playerId = player.getUniqueId();
boolean skillFail = isSkillFail(playerId);
-
- String skillFailMsg = "";
-
- // Show bleeding warning if bleeding
+
+ String skillFailMsg = switch (tool) {
+ case SPONGE -> handleSponge(player, menu, playerId, skillFail);
+ case SCALPEL -> handleScalpel(player, menu, playerId, skillFail);
+ case SUTURE -> handleSuture(player, menu, playerId, skillFail);
+ case TINCTURE -> handleTincture(player, menu, playerId, skillFail);
+ case CARBOLIC_ACID -> handleCarbolicAcid(player, menu, playerId, skillFail);
+ case DRESSING -> handleDressing(player, menu, playerId, skillFail);
+ case STETHOSCOPE -> handleStethoscope(player, menu, playerId, skillFail);
+ case THERMOMETER -> handleThermometer(menu, skillFail);
+ case CHLOROFORM -> handleChloroform(player, menu, playerId, skillFail);
+ case SMELLING_SALTS -> handleSmellingSalts(menu, playerId, skillFail);
+ case SILVER_WIRE -> handleSilverWire(player, menu, playerId, skillFail);
+ case SPLINT -> handleSplint(player, menu, playerId, skillFail);
+ case ARTERY_FORCEPS -> handleArteryForceps(player, menu, playerId, skillFail);
+ case TRANSFUSION -> handleTransfusion(menu, playerId, skillFail);
+ };
+
+ // Failing to use chloroform correctly can end the surgery outright
+ if (!stateManager.hasSession(playerId)) {
+ return;
+ }
+
if (stateManager.isBleeding(playerId)) {
uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("bleeding-warning"));
}
-
- // ==============================================
- // Skill fail checks for each item
- // ==============================================
- switch (clickedSlot) {
- case SurgeryConstants.SLOT_SPONGE: skillFailMsg = handleSponge(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_SCALPEL: skillFailMsg = handleScalpel(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_STITCHES: skillFailMsg = handleStitches(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_ANTIBIOTICS: skillFailMsg = handleAntibiotics(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_ANTISEPTIC: skillFailMsg = handleAntiseptic(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_SURGICAL_GLOVE: skillFailMsg = handleSurgicalGlove(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_ULTRASOUND: skillFailMsg = handleUltrasound(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_LAB_KIT: skillFailMsg = handleLabKit(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_ANESTHETIC: skillFailMsg = handleAnesthetic(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_DEFIBRILLATOR: skillFailMsg = handleDefibrillator(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_PINS: skillFailMsg = handlePins(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_SPLINT: skillFailMsg = handleSplint(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_CLAMP: skillFailMsg = handleClamp(player, menu, playerId, skillFail); break;
- case SurgeryConstants.SLOT_TRANSFUSION: skillFailMsg = handleTransfusion(player, menu, playerId, skillFail); break;
- }
-
+
// ==============================================
// Update skill fail block
// ==============================================
@@ -179,396 +149,360 @@ private void updateMenu(Player player, int clickedSlot) {
String skillFailDisplay = skillFailMsg.isEmpty() ? ChatColor.GRAY + "Nothing to show here" : ChatColor.GRAY + skillFailMsg;
ItemStack skillFailBlock = uiUpdater.createInfoBlock(skillFailColor, ChatColor.GOLD + "Skill Fail", skillFailDisplay);
menu.setItem(SurgeryConstants.SLOT_SKILL_FAIL, skillFailBlock);
-
+
// Clear sponge effect (it only lasts for one move)
stateManager.setSpongeEffect(playerId, false);
-
+
// Play "broken item" sound if skill fail occurred
if (!skillFailMsg.isEmpty()) {
player.playSound(player.getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 1.0f);
}
-
- // Check if surgery is complete after this move (if diagnosis was already cured)
+
+ // Check if surgery is complete after this move (if the ailment was already treated)
if (stateManager.isCured(playerId) && completionHandler.isSurgerySuccessful(playerId)) {
completionHandler.handleSuccess(player);
}
}
-
+
// ==============================================
- // Labkit functionality: reveals antibiotics
+ // Clinical thermometer: reveals the fever tincture
// ==============================================
- private String handleLabKit(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleThermometer(Inventory menu, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailLabKit);
- } else {
- menu.setItem(SurgeryConstants.SLOT_LAB_KIT, null);
- ItemStack antibiotics = api.getCreator().getItemFromPath(itemsConfig.getItemPath(3));
- if (antibiotics != null) {
- menu.setItem(SurgeryConstants.SLOT_ANTIBIOTICS, antibiotics);
- }
- return "";
+ return getRandomSkillFail(SurgeryTool.THERMOMETER);
+ }
+ menu.setItem(SurgeryTool.THERMOMETER.getSlot(), null);
+ ItemStack tincture = api.getCreator().getItemFromPath(itemsConfig.getItemPath(SurgeryTool.TINCTURE));
+ if (tincture != null) {
+ menu.setItem(SurgeryTool.TINCTURE.getSlot(), tincture);
}
+ return "";
}
-
+
// ==============================================
- // Ultrasound functionality: reveals diagnosis and sets temperature for flu diagnoses
+ // Stethoscope: examines the patient, revealing the procedure and its bones
// ==============================================
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
- private String handleUltrasound(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleStethoscope(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailUltrasound);
- } else {
- menu.setItem(SurgeryConstants.SLOT_ULTRASOUND, null);
- String diagnosis = diagnosesList.get(ThreadLocalRandom.current().nextInt(diagnosesList.size()));
- stateManager.setDiagnosis(playerId, diagnosis);
-
- // If diagnosis is a flu, set initial high temperature from config range
- if (diagnosisChecker.isFlu(diagnosis)) {
- double fluMin = plugin.getConfig().getDouble("temperature.flu-temp-min", 99.0);
- double fluMax = plugin.getConfig().getDouble("temperature.flu-temp-max", 104.0);
- double fluTemp = fluMin + (ThreadLocalRandom.current().nextDouble() * (fluMax - fluMin));
- stateManager.setTemperature(playerId, fluTemp);
- uiUpdater.updateTemperatureBlock(menu, playerId, fluTemp);
- }
-
- // Assign bones only for bone-related diagnoses
- if (diagnosisChecker.hasBones(diagnosis)) {
- // Get bone counts from config, or use random if not specified
- int brokenBones = plugin.getConfig().getInt("bone-counts." + diagnosis + ".broken", ThreadLocalRandom.current().nextInt(3));
- int shatteredBones = plugin.getConfig().getInt("bone-counts." + diagnosis + ".shattered", ThreadLocalRandom.current().nextInt(2));
- stateManager.setBrokenBones(playerId, brokenBones);
- stateManager.setShatteredBones(playerId, shatteredBones);
- }
-
- // Update diagnosis block
- ItemStack diagnosisBlock = uiUpdater.createInfoBlock(Material.YELLOW_CONCRETE,
- ChatColor.GOLD + "Diagnosis", ChatColor.GRAY + "The patient suffers from " + diagnosis);
- menu.setItem(SurgeryConstants.SLOT_DIAGNOSIS, diagnosisBlock);
-
- // Check if surgical glove should appear
- mechanicsManager.checkForFixItButton(player, menu, playerId, 0);
- return "";
+ return getRandomSkillFail(SurgeryTool.STETHOSCOPE);
+ }
+ menu.setItem(SurgeryTool.STETHOSCOPE.getSlot(), null);
+ stateManager.setExamined(playerId, true);
+ Procedure procedure = stateManager.getProcedure(playerId);
+
+ // Sepsis: the patient is already feverish and the fever keeps climbing
+ if (procedure.has(Complication.SEPSIS)) {
+ double feverMin = plugin.getConfig().getDouble("complications.sepsis.fever-min", 100.0);
+ double feverMax = plugin.getConfig().getDouble("complications.sepsis.fever-max", 104.0);
+ double fever = Math.max(stateManager.getTemperature(playerId),
+ feverMin + ThreadLocalRandom.current().nextDouble() * (feverMax - feverMin));
+ stateManager.setHasRisingTemp(playerId, true);
+ stateManager.setTemperature(playerId, fever);
+ uiUpdater.updateTemperatureBlock(menu, playerId, fever);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("sepsis-found"));
}
+
+ stateManager.setBrokenBones(playerId, procedure.brokenBones());
+ stateManager.setShatteredBones(playerId, procedure.shatteredBones());
+ uiUpdater.updateDiagnosisBlock(menu, playerId);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("examined")
+ .replace("%procedure%", procedure.name())
+ .replace("%incisions%", String.valueOf(procedure.requiredIncisions())));
+
+ // Incisions made before the examination still count
+ int incisions = stateManager.getIncisions(playerId);
+ mechanicsManager.handleBoneReveal(player, menu, playerId, incisions);
+ mechanicsManager.checkForDressing(player, menu, playerId, incisions);
+ return "";
}
-
+
// ==============================================
- // Scalpel functionality: creates incisions and affects pulse
+ // Scalpel: creates incisions and affects pulse
// ==============================================
private String handleScalpel(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
// On skill fail: lower pulse instead of creating incision
- String currentPulse = stateManager.getPulse(playerId);
- String newPulse = SurgeryConstants.worsenPulse(currentPulse);
- stateManager.setPulse(playerId, newPulse);
- uiUpdater.updatePulseBlock(menu, playerId, newPulse);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("pulse-weakening"));
- return getRandomSkillFail(skillFailScalpel);
- } else {
- int incisions = stateManager.getIncisions(playerId) + 1;
- stateManager.setIncisions(playerId, incisions);
- uiUpdater.updateIncisionBlock(menu, playerId, incisions);
-
- // Scalpel makes operation site unclean ("bleeding")
- String currentSite = stateManager.getOperationSite(playerId);
- if (currentSite.equals("Clean")) {
- stateManager.setOperationSite(playerId, "Unclean");
- uiUpdater.updateOperationSiteBlock(menu, playerId, "Unclean");
- }
-
- // Paper Cuts: Show examined message after configured scalpel uses
- String diagnosis = stateManager.getDiagnosis(playerId);
- int paperCutsUses = plugin.getConfig().getInt("diagnosis-mechanics.paper-cuts.scalpel-uses-required", 2);
- if (diagnosis != null && diagnosis.equals("Paper Cuts") && incisions == paperCutsUses) {
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("wounds-examined"));
- }
-
- // Reveal bones at required incision count for bone-based diagnoses
- if (diagnosis != null) {
- mechanicsManager.handleBoneReveal(player, menu, playerId, diagnosis, incisions);
- }
+ worsenPulse(player, menu, playerId);
+ return getRandomSkillFail(SurgeryTool.SCALPEL);
+ }
+ // Once the patient has been sedated or cut, walking away counts as a failed surgery
+ stateManager.setOperated(playerId, true);
+ int incisions = stateManager.getIncisions(playerId) + 1;
+ stateManager.setIncisions(playerId, incisions);
+ uiUpdater.updateIncisionBlock(menu, playerId, incisions);
- // Always refresh dynamic tools; the clamp depends on incision count and
- // bleeding, which can both be true before a diagnosis exists
- mechanicsManager.updateDynamicTools(player, menu, playerId);
-
- // 50% chance for pulse to decrease when making incision
- if (ThreadLocalRandom.current().nextDouble() < plugin.getConfig().getDouble("pulse.scalpel-decrease-chance", 0.50)) {
- String currentPulse = stateManager.getPulse(playerId);
- String newPulse = SurgeryConstants.worsenPulse(currentPulse);
- stateManager.setPulse(playerId, newPulse);
- uiUpdater.updatePulseBlock(menu, playerId, newPulse);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("pulse-weakening"));
- }
-
- mechanicsManager.checkForFixItButton(player, menu, playerId, incisions);
- return "";
+ // Cutting makes a clean operation site unclean
+ if (stateManager.getOperationSite(playerId).equals("Clean")) {
+ stateManager.setOperationSite(playerId, "Unclean");
+ uiUpdater.updateOperationSiteBlock(menu, playerId, "Unclean");
}
+
+ // Reveal bones at the procedure's incision count
+ mechanicsManager.handleBoneReveal(player, menu, playerId, incisions);
+
+ // Always refresh dynamic tools; the forceps depend on incision count and
+ // bleeding, which can both be true before the examination
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
+
+ // Chance for pulse to decrease when making incision
+ if (ThreadLocalRandom.current().nextDouble() < plugin.getConfig().getDouble("pulse.scalpel-decrease-chance", 0.50)) {
+ worsenPulse(player, menu, playerId);
+ }
+
+ mechanicsManager.checkForDressing(player, menu, playerId, incisions);
+ return "";
+ }
+
+ private void worsenPulse(Player player, Inventory menu, UUID playerId) {
+ String newPulse = SurgeryConstants.worsenPulse(stateManager.getPulse(playerId));
+ stateManager.setPulse(playerId, newPulse);
+ uiUpdater.updatePulseBlock(menu, playerId, newPulse);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("pulse-weakening"));
}
-
+
// ==============================================
- // Stitches functionality: closes incisions and can stop bleeding
+ // Catgut suture: closes incisions and can stop bleeding
// ==============================================
- private String handleStitches(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleSuture(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailStitches);
- } else {
- int incisions = Math.max(0, stateManager.getIncisions(playerId) - 1);
- stateManager.setIncisions(playerId, incisions);
- uiUpdater.updateIncisionBlock(menu, playerId, incisions);
- if (incisions == 0) {
- stateManager.setBleeding(playerId, false);
- }
- mechanicsManager.updateDynamicTools(player, menu, playerId);
- return "";
+ return getRandomSkillFail(SurgeryTool.SUTURE);
+ }
+ int incisions = Math.max(0, stateManager.getIncisions(playerId) - 1);
+ stateManager.setIncisions(playerId, incisions);
+ uiUpdater.updateIncisionBlock(menu, playerId, incisions);
+ if (incisions == 0) {
+ stateManager.setBleeding(playerId, false);
}
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
+ return "";
}
-
+
// ==============================================
- // Antibiotics functionality: reduces temperature by 5.4°F (3.0°C)
+ // Willow-bark tincture: brings the fever down
// ==============================================
- private String handleAntibiotics(Player player, Inventory menu, UUID playerId, boolean skillFail) {
- double change = plugin.getConfig().getDouble("temperature.antibiotics-change", 5.4);
+ private String handleTincture(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ double change = plugin.getConfig().getDouble("temperature.tincture-change", 5.4);
if (skillFail) {
double temp = stateManager.getTemperature(playerId) + change;
temp = Math.min(temp, plugin.getConfig().getDouble("temperature.instant-death-threshold", 110.0));
stateManager.setTemperature(playerId, temp);
uiUpdater.updateTemperatureBlock(menu, playerId, temp);
- return getRandomSkillFail(skillFailAntibiotics);
- } else {
- double temp = stateManager.getTemperature(playerId) - change;
- temp = Math.max(temp, plugin.getConfig().getDouble("temperature.normal", 98.6));
- stateManager.setTemperature(playerId, temp);
- uiUpdater.updateTemperatureBlock(menu, playerId, temp);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("temperature-reduced"));
- mechanicsManager.checkForFixItButton(player, menu, playerId, 0);
- return "";
+ return getRandomSkillFail(SurgeryTool.TINCTURE);
}
+ double temp = stateManager.getTemperature(playerId) - change;
+ temp = Math.max(temp, plugin.getConfig().getDouble("temperature.normal", 98.6));
+ stateManager.setTemperature(playerId, temp);
+ uiUpdater.updateTemperatureBlock(menu, playerId, temp);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("temperature-reduced"));
+ return "";
}
-
+
// ==============================================
- // Transfusion functionality: improves pulse
+ // Transfusion syringe: improves pulse
// ==============================================
- private String handleTransfusion(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleTransfusion(Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
stateManager.setOperationSite(playerId, "Unsanitary");
uiUpdater.updateOperationSiteBlock(menu, playerId, "Unsanitary");
- return getRandomSkillFail(skillFailTransfusion);
- } else {
- String currentPulse = stateManager.getPulse(playerId);
- String newPulse = SurgeryConstants.improvePulse(currentPulse);
- stateManager.setPulse(playerId, newPulse);
- uiUpdater.updatePulseBlock(menu, playerId, newPulse);
- return "";
+ return getRandomSkillFail(SurgeryTool.TRANSFUSION);
}
+ String newPulse = SurgeryConstants.improvePulse(stateManager.getPulse(playerId));
+ stateManager.setPulse(playerId, newPulse);
+ uiUpdater.updatePulseBlock(menu, playerId, newPulse);
+ return "";
}
-
+
// ==============================================
- // Antiseptic functionality: cleans the operation site and provides temperature protection
+ // Carbolic acid: cleans the operation site and protects against fever
// ==============================================
- private String handleAntiseptic(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleCarbolicAcid(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailAntiseptic);
- } else {
- stateManager.setOperationSite(playerId, "Clean");
- uiUpdater.updateOperationSiteBlock(menu, playerId, "Clean");
- stateManager.setAntisepticProtection(playerId, true);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("operation-clean"));
- return "";
+ return getRandomSkillFail(SurgeryTool.CARBOLIC_ACID);
}
+ stateManager.setOperationSite(playerId, "Clean");
+ uiUpdater.updateOperationSiteBlock(menu, playerId, "Clean");
+ stateManager.setAntisepticProtection(playerId, true);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("operation-clean"));
+ return "";
}
-
+
// ==============================================
- // Sponge functionality: removes bleeding and provides temporary protection against skill fails
+ // Sponge: removes bleeding and reduces the next move's skill fail chance
// ==============================================
private String handleSponge(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailSponge);
- } else {
- stateManager.setBleeding(playerId, false);
- stateManager.setSpongeEffect(playerId, true);
- stateManager.setMovesSinceLastSponge(playerId, 0);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("vision-cleared"));
- return "";
+ return getRandomSkillFail(SurgeryTool.SPONGE);
}
+ stateManager.setBleeding(playerId, false);
+ stateManager.setSpongeEffect(playerId, true);
+ stateManager.setMovesSinceLastSponge(playerId, 0);
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("vision-cleared"));
+ return "";
}
-
+
// ==============================================
- // Anesthetic functionality: unconsciousness for the patient
+ // Chloroform: puts the patient under
// ==============================================
- private String handleAnesthetic(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleChloroform(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailAnesthetic);
- } else {
- Integer unconsciousTimer = stateManager.getUnconsciousTimer(playerId);
- String currentStatus = stateManager.getStatus(playerId);
-
- int reuseCooldown = plugin.getConfig().getInt("death-timers.anesthetic-reuse-cooldown", 4);
- if ((currentStatus.equals("Unconscious") || currentStatus.equals("Coming to")) &&
- unconsciousTimer != null && unconsciousTimer < reuseCooldown) {
- completionHandler.failSurgery(player, uiUpdater.getMessage("failure-anesthetic-misuse"));
- return "";
- }
-
- stateManager.setStatus(playerId, "Unconscious");
- uiUpdater.updateStatusBlock(menu, playerId, "Unconscious");
- stateManager.setUnconsciousTimer(playerId, 0);
+ return getRandomSkillFail(SurgeryTool.CHLOROFORM);
+ }
+ Integer unconsciousTimer = stateManager.getUnconsciousTimer(playerId);
+ String currentStatus = stateManager.getStatus(playerId);
+
+ int reuseCooldown = plugin.getConfig().getInt("death-timers.anesthetic-reuse-cooldown", 4);
+ if ((currentStatus.equals("Unconscious") || currentStatus.equals("Coming to")) &&
+ unconsciousTimer != null && unconsciousTimer < reuseCooldown) {
+ completionHandler.failSurgery(player, uiUpdater.getMessage("failure-anesthetic-misuse"));
return "";
}
+
+ stateManager.setStatus(playerId, "Unconscious");
+ uiUpdater.updateStatusBlock(menu, playerId, "Unconscious");
+ stateManager.setUnconsciousTimer(playerId, 0);
+ stateManager.setOperated(playerId, true);
+ return "";
}
-
+
// ==============================================
- // Defibrillator functionality: revives the patient from a stopped heart
+ // Smelling salts: revive a collapsed patient
// ==============================================
- private String handleDefibrillator(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleSmellingSalts(Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailDefibrillator);
- } else {
- String status = stateManager.getStatus(playerId);
- if (status.equals("Heart Stopped")) {
- stateManager.setStatus(playerId, "Unconscious");
- uiUpdater.updateStatusBlock(menu, playerId, "Unconscious");
- stateManager.removeDefibrillatorCountdown(playerId);
- menu.setItem(SurgeryConstants.SLOT_DEFIBRILLATOR, null);
- }
- return "";
+ return getRandomSkillFail(SurgeryTool.SMELLING_SALTS);
}
+ if (stateManager.getStatus(playerId).equals("Collapsed")) {
+ stateManager.setStatus(playerId, "Unconscious");
+ uiUpdater.updateStatusBlock(menu, playerId, "Unconscious");
+ stateManager.removeCollapseCountdown(playerId);
+ menu.setItem(SurgeryTool.SMELLING_SALTS.getSlot(), null);
+ }
+ return "";
}
-
+
// ==============================================
- // Splint functionality: fixes broken bones
+ // Splint: sets broken bones
// ==============================================
private String handleSplint(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
stateManager.setBleeding(playerId, true);
- return getRandomSkillFail(skillFailSplint);
- } else {
- int actualBroken = stateManager.getBrokenBones(playerId);
- if (actualBroken > 0) {
- stateManager.setBrokenBones(playerId, actualBroken - 1);
- int revealedBroken = stateManager.getRevealedBrokenBones(playerId);
- if (revealedBroken > 0) {
- stateManager.setRevealedBrokenBones(playerId, revealedBroken - 1);
- }
- uiUpdater.updateDiagnosisBlock(menu, playerId);
- if (actualBroken - 1 == 0) {
- menu.setItem(SurgeryConstants.SLOT_SPLINT, null);
- }
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
+ return getRandomSkillFail(SurgeryTool.SPLINT);
+ }
+ int actualBroken = stateManager.getBrokenBones(playerId);
+ if (actualBroken > 0) {
+ stateManager.setBrokenBones(playerId, actualBroken - 1);
+ int revealedBroken = stateManager.getRevealedBrokenBones(playerId);
+ if (revealedBroken > 0) {
+ stateManager.setRevealedBrokenBones(playerId, revealedBroken - 1);
}
- return "";
+ uiUpdater.updateDiagnosisBlock(menu, playerId);
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
+ mechanicsManager.checkForDressing(player, menu, playerId, stateManager.getIncisions(playerId));
}
+ return "";
}
-
+
// ==============================================
- // Pins functionality: Fixes shattered bones
+ // Silver wire: binds shattered bones into broken bones that can be splinted
// ==============================================
- private String handlePins(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleSilverWire(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- String diagnosis = stateManager.getDiagnosis(playerId);
- if (diagnosis != null && diagnosis.equals("Ecto-Bones")) {
- int shatteredBones = stateManager.getShatteredBones(playerId) + 1;
- stateManager.setShatteredBones(playerId, shatteredBones);
- int revealedShattered = stateManager.getRevealedShatteredBones(playerId) + 1;
- stateManager.setRevealedShatteredBones(playerId, revealedShattered);
- return uiUpdater.getMessage("skill-fail-pins-ecto-bones");
- } else {
- stateManager.setBleeding(playerId, true);
- return getRandomSkillFail(skillFailPins);
- }
- } else {
- int shatteredBones = stateManager.getShatteredBones(playerId);
- if (shatteredBones > 0) {
- int brokenBones = stateManager.getBrokenBones(playerId);
- stateManager.setBrokenBones(playerId, brokenBones + 1);
- stateManager.setShatteredBones(playerId, shatteredBones - 1);
-
- int revealedShattered = stateManager.getRevealedShatteredBones(playerId);
- int revealedBroken = stateManager.getRevealedBrokenBones(playerId);
- if (revealedShattered > 0) {
- stateManager.setRevealedShatteredBones(playerId, revealedShattered - 1);
- }
- stateManager.setRevealedBrokenBones(playerId, revealedBroken + 1);
-
- uiUpdater.updateDiagnosisBlock(menu, playerId);
- mechanicsManager.updateDynamicTools(player, menu, playerId);
-
- if (shatteredBones - 1 == 0) {
- menu.setItem(SurgeryConstants.SLOT_PINS, null);
- }
+ stateManager.setBleeding(playerId, true);
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
+ return getRandomSkillFail(SurgeryTool.SILVER_WIRE);
+ }
+ int shatteredBones = stateManager.getShatteredBones(playerId);
+ if (shatteredBones > 0) {
+ stateManager.setBrokenBones(playerId, stateManager.getBrokenBones(playerId) + 1);
+ stateManager.setShatteredBones(playerId, shatteredBones - 1);
+
+ int revealedShattered = stateManager.getRevealedShatteredBones(playerId);
+ if (revealedShattered > 0) {
+ stateManager.setRevealedShatteredBones(playerId, revealedShattered - 1);
}
- return "";
+ stateManager.setRevealedBrokenBones(playerId, stateManager.getRevealedBrokenBones(playerId) + 1);
+
+ uiUpdater.updateDiagnosisBlock(menu, playerId);
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
}
+ return "";
}
-
+
// ==============================================
- // Clamp functionality: stops bleeding
+ // Artery forceps: stop bleeding from an open wound
// ==============================================
- private String handleClamp(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleArteryForceps(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailClamp);
- } else {
- if (stateManager.getIncisions(playerId) > 0 && stateManager.isBleeding(playerId)) {
- stateManager.setBleeding(playerId, false);
- mechanicsManager.updateDynamicTools(player, menu, playerId);
- }
- return "";
+ return getRandomSkillFail(SurgeryTool.ARTERY_FORCEPS);
}
+ if (stateManager.getIncisions(playerId) > 0 && stateManager.isBleeding(playerId)) {
+ stateManager.setBleeding(playerId, false);
+ mechanicsManager.updateDynamicTools(player, menu, playerId);
+ }
+ return "";
}
-
+
// ==============================================
- // Surgical Glove functionality: "Fixes" the patient
+ // Linen dressing: treats the ailment once the procedure is done
// ==============================================
- private String handleSurgicalGlove(Player player, Inventory menu, UUID playerId, boolean skillFail) {
+ private String handleDressing(Player player, Inventory menu, UUID playerId, boolean skillFail) {
if (skillFail) {
- return getRandomSkillFail(skillFailSurgicalGlove);
- } else {
- stateManager.setCured(playerId, true);
- menu.setItem(SurgeryConstants.SLOT_SURGICAL_GLOVE, null);
- uiUpdater.updateDiagnosisBlock(menu, playerId);
-
- // Don't show incomplete message if surgery is already successful
- if (!completionHandler.isSurgerySuccessful(playerId)) {
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("condition-treated-incomplete"));
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("check-remaining"));
- }
- return "";
+ return getRandomSkillFail(SurgeryTool.DRESSING);
}
+ stateManager.setCured(playerId, true);
+ menu.setItem(SurgeryTool.DRESSING.getSlot(), null);
+ uiUpdater.updateDiagnosisBlock(menu, playerId);
+
+ // Don't show incomplete message if surgery is already successful
+ if (!completionHandler.isSurgerySuccessful(playerId)) {
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("condition-treated-incomplete"));
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("check-remaining"));
+ }
+ return "";
}
-
+
// ==============================================
- // Gets a random skill fail message from a list
+ // Gets a random skill fail message for a tool
// ==============================================
- private String getRandomSkillFail(java.util.List messages) {
+ private String getRandomSkillFail(SurgeryTool tool) {
+ List messages = skillFailMessages.get(tool);
if (messages == null || messages.isEmpty()) {
return "Something went wrong!";
}
return messages.get(ThreadLocalRandom.current().nextInt(messages.size()));
}
-
+
// ==============================================
// Checks if skill fail should occur based on bleeding and sponge effect
// ==============================================
private boolean isSkillFail(UUID playerId) {
double baseChance = plugin.getConfig().getDouble("skill-fail.base-chance", 0.25);
-
+
if (stateManager.isBleeding(playerId)) {
baseChance = plugin.getConfig().getDouble("skill-fail.bleeding-chance", 0.40);
}
-
+
if (stateManager.hasSpongeEffect(playerId)) {
baseChance = plugin.getConfig().getDouble("skill-fail.with-sponge-chance", 0.10);
}
-
+
return ThreadLocalRandom.current().nextDouble() < baseChance;
}
-
+
// ==============================================
- // Removes item from player inventory
+ // Removes one of the tool from the player's inventory
+ // Matches by item path rather than exact meta, so tools crafted before a
+ // rename or lore change still count
// ==============================================
- private boolean removeItemFromPlayer(Player player, ItemStack targetItem) {
+ private boolean removeItemFromPlayer(Player player, SurgeryTool tool) {
+ String path = itemsConfig.getItemPath(tool);
for (int i = 0; i < player.getInventory().getSize(); i++) {
ItemStack item = player.getInventory().getItem(i);
- if (item != null && item.isSimilar(targetItem)) {
+ if (item != null && api.getChecker().checkItemWithPath(item, path)) {
if (item.getAmount() > 1) {
item.setAmount(item.getAmount() - 1);
} else {
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemsConfig.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemsConfig.java
index 28198b7..e34bbc0 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemsConfig.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryItemsConfig.java
@@ -8,29 +8,28 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
+import java.util.EnumMap;
+import java.util.Map;
// ==============================================
// Configuration for surgery item paths
// ==============================================
public class SurgeryItemsConfig {
-
+
private final JavaPlugin plugin;
- private FileConfiguration config;
-
- // Item paths array (index matches original SURGERY_ITEMS)
- private final String[] itemPaths = new String[14];
-
+ private final Map itemPaths = new EnumMap<>(SurgeryTool.class);
+
public SurgeryItemsConfig(JavaPlugin plugin) {
this.plugin = plugin;
loadConfig();
}
-
+
// ==============================================
// Load or create the surgeryItemsConfig.yml
// ==============================================
private void loadConfig() {
File configFile = new File(plugin.getDataFolder(), "surgeryItemsConfig.yml");
-
+
// Create config file if it doesn't exist
if (!configFile.exists()) {
plugin.getDataFolder().mkdirs();
@@ -42,41 +41,17 @@ private void loadConfig() {
plugin.getLogger().severe("Could not create surgeryItemsConfig.yml: " + e.getMessage());
}
}
-
- // Load the config
- config = YamlConfiguration.loadConfiguration(configFile);
-
- // Load item paths into array
- itemPaths[0] = config.getString("items.sponge", "m.surgery.sponge");
- itemPaths[1] = config.getString("items.scalpel", "m.surgery.scalpel");
- itemPaths[2] = config.getString("items.stitches", "m.surgery.stitches");
- itemPaths[3] = config.getString("items.antibiotics", "m.surgery.antibiotics");
- itemPaths[4] = config.getString("items.antiseptic", "m.surgery.antiseptic");
- itemPaths[5] = config.getString("items.surgical-glove", "m.surgery.surgical_glove");
- itemPaths[6] = config.getString("items.ultrasound", "m.surgery.ultrasound");
- itemPaths[7] = config.getString("items.lab-kit", "m.surgery.lab_kit");
- itemPaths[8] = config.getString("items.anesthetic", "m.surgery.anesthetic");
- itemPaths[9] = config.getString("items.defibrillator", "m.surgery.defibrillator");
- itemPaths[10] = config.getString("items.pins", "m.surgery.pins");
- itemPaths[11] = config.getString("items.splint", "m.surgery.splint");
- itemPaths[12] = config.getString("items.clamp", "m.surgery.clamp");
- itemPaths[13] = config.getString("items.transfusion", "m.surgery.transfusion");
- }
-
- // ==============================================
- // Get the item paths array
- // ==============================================
- public String[] getItemPaths() {
- return itemPaths;
+
+ FileConfiguration config = YamlConfiguration.loadConfiguration(configFile);
+ for (SurgeryTool tool : SurgeryTool.values()) {
+ itemPaths.put(tool, config.getString("items." + tool.getConfigKey(), tool.getDefaultPath()));
+ }
}
-
+
// ==============================================
- // Get a specific item path by index
+ // Get the TLibs item path for a tool
// ==============================================
- public String getItemPath(int index) {
- if (index >= 0 && index < itemPaths.length) {
- return itemPaths[index];
- }
- return null;
+ public String getItemPath(SurgeryTool tool) {
+ return itemPaths.get(tool);
}
}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMechanicsManager.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMechanicsManager.java
index ea8fe9f..5f1d0e2 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMechanicsManager.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMechanicsManager.java
@@ -1,5 +1,7 @@
package net.tfminecraft.surgery.managers;
+import net.tfminecraft.surgery.procedures.Complication;
+import net.tfminecraft.surgery.procedures.Procedure;
import net.tfminecraft.tlibs.objects.api.ItemAPI;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -7,58 +9,38 @@
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
-import java.util.HashMap;
-import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
// ==============================================
-// Handles diagnosis-specific mechanics and per-move effects
+// Handles complications and per-move effects
// ==============================================
public class SurgeryMechanicsManager {
-
+
private final JavaPlugin plugin;
private final ItemAPI api;
private final SurgeryStateManager stateManager;
private final SurgeryUIUpdater uiUpdater;
private final SurgeryCompletionHandler completionHandler;
- private final DiagnosisChecker diagnosisChecker;
private final SurgeryItemsConfig itemsConfig;
- // Map diagnoses to required incision counts
- private final Map requiredIncisions = new HashMap<>();
-
public SurgeryMechanicsManager(JavaPlugin plugin, ItemAPI api, SurgeryStateManager stateManager,
SurgeryUIUpdater uiUpdater, SurgeryCompletionHandler completionHandler,
- DiagnosisChecker diagnosisChecker, SurgeryItemsConfig itemsConfig) {
+ SurgeryItemsConfig itemsConfig) {
this.plugin = plugin;
this.api = api;
this.stateManager = stateManager;
this.uiUpdater = uiUpdater;
this.completionHandler = completionHandler;
- this.diagnosisChecker = diagnosisChecker;
this.itemsConfig = itemsConfig;
}
-
- // ==============================================
- // Initializes required incisions from config
- // ==============================================
- public void initialize() {
- var config = plugin.getConfig().getConfigurationSection("required-incisions");
- if (config != null) {
- for (String key : config.getKeys(false)) {
- requiredIncisions.put(key, config.getInt(key));
- }
- }
- }
-
+
// ==============================================
- // Processes per-move effects (antibiotics countdown, temperature changes, etc.)
+ // Processes per-move effects (anesthetic wear-off, fever, blood loss, etc.)
// ==============================================
- public void processMoveEffects(Player player, int clickedSlot) {
+ public void processMoveEffects(Player player, SurgeryTool clickedTool) {
UUID playerId = player.getUniqueId();
Inventory menu = player.getOpenInventory().getTopInventory();
- String diagnosis = stateManager.getDiagnosis(playerId);
// Fail if the patient logged off or moved out of surgery range
if (!isPatientPresent(player)) {
@@ -75,7 +57,7 @@ public void processMoveEffects(Player player, int clickedSlot) {
int movesSinceSponge = stateManager.getMovesSinceLastSponge(playerId) + 1;
stateManager.setMovesSinceLastSponge(playerId, movesSinceSponge);
- // Advance the anesthetic wear-off cycle: Unconscious -> Coming to -> Awake
+ // Advance the chloroform wear-off cycle: Unconscious -> Coming to -> Awake
String currentStatus = stateManager.getStatus(playerId);
if (currentStatus.equals("Unconscious") || currentStatus.equals("Coming to")) {
Integer unconsciousTimer = stateManager.getUnconsciousTimer(playerId);
@@ -90,32 +72,32 @@ public void processMoveEffects(Player player, int clickedSlot) {
stateManager.setStatus(playerId, "Coming to");
uiUpdater.updateStatusBlock(menu, playerId, "Coming to");
uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("patient-coming-to",
- "&eThe anesthetic is wearing off - the patient is coming to!"));
+ "&eThe chloroform is wearing off - the patient is coming to!"));
} else if (currentStatus.equals("Coming to") && timer >= unconsciousMoves + comingToMoves) {
stateManager.setStatus(playerId, "Awake");
uiUpdater.updateStatusBlock(menu, playerId, "Awake");
uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("patient-woke-up",
- "&cThe patient is fully awake! Re-administer anesthetic before cutting!"));
+ "&cThe patient is fully awake! Give more chloroform before cutting!"));
}
}
}
-
- // Check defibrillator countdown (failure after configured moves)
- // Skip the decrement when the defibrillator itself was clicked, otherwise
- // the countdown expires before the shock is applied and the full window
+
+ // Check the collapse countdown (failure after configured moves)
+ // Skip the decrement when the smelling salts were used, otherwise the
+ // countdown expires before the patient is revived and the full window
// promised by the config is never usable
- if (currentStatus.equals("Heart Stopped") && clickedSlot != SurgeryConstants.SLOT_DEFIBRILLATOR) {
- Integer countdown = stateManager.getDefibrillatorCountdown(playerId);
+ if (currentStatus.equals("Collapsed") && clickedTool != SurgeryTool.SMELLING_SALTS) {
+ Integer countdown = stateManager.getCollapseCountdown(playerId);
if (countdown != null) {
countdown--;
if (countdown <= 0) {
completionHandler.failSurgery(player, uiUpdater.getMessage("failure-not-resuscitated"));
return;
}
- stateManager.setDefibrillatorCountdown(playerId, countdown);
+ stateManager.setCollapseCountdown(playerId, countdown);
}
}
-
+
// Check if temperature reached instant death threshold
// >= because every temperature increase clamps to the threshold, so a
// strict > could never fire and the mechanic was dead
@@ -125,7 +107,7 @@ public void processMoveEffects(Player player, int clickedSlot) {
completionHandler.failSurgery(player, uiUpdater.getMessage("failure-infection"));
return;
}
-
+
// Check for consecutive red temperature. Fail after configured turns
double redTempThreshold = plugin.getConfig().getDouble("temperature.red-temp-threshold", 106.0);
int maxRedTempTurns = plugin.getConfig().getInt("death-timers.red-temp-turns", 2);
@@ -139,7 +121,7 @@ public void processMoveEffects(Player player, int clickedSlot) {
} else {
stateManager.setRedTempCounter(playerId, 0);
}
-
+
// Degrade pulse if bleeding
double pulseDegradationChance = plugin.getConfig().getDouble("pulse.degradation-chance-bleeding", 0.30);
if (stateManager.isBleeding(playerId) && ThreadLocalRandom.current().nextDouble() < pulseDegradationChance) {
@@ -153,7 +135,7 @@ public void processMoveEffects(Player player, int clickedSlot) {
uiUpdater.updatePulseBlock(menu, playerId, newPulse);
uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("pulse-weakening"));
}
-
+
// Check for consecutive Extremely Weak pulse. Fail after configured turns
String currentPulse = stateManager.getPulse(playerId);
int maxWeakPulseTurns = plugin.getConfig().getInt("death-timers.weak-pulse-turns", 2);
@@ -167,16 +149,16 @@ public void processMoveEffects(Player player, int clickedSlot) {
} else {
stateManager.setExtremelyWeakCounter(playerId, 0);
}
-
+
// Handle temperature rise
String opSite = stateManager.getOperationSite(playerId);
boolean hasProtection = stateManager.hasAntisepticProtection(playerId);
boolean isBleeding = stateManager.isBleeding(playerId);
int incisions = stateManager.getIncisions(playerId);
boolean hasRisingTemp = stateManager.hasRisingTemp(playerId);
-
+
boolean shouldRiseTemp = (!opSite.equals("Clean") && (incisions > 0 || isBleeding)) || hasRisingTemp;
-
+
if (shouldRiseTemp && !hasProtection) {
double riseRate = plugin.getConfig().getDouble("temperature.rise-rate", 1.8);
double maxTemp = plugin.getConfig().getDouble("temperature.instant-death-threshold", 110.0);
@@ -185,18 +167,20 @@ public void processMoveEffects(Player player, int clickedSlot) {
stateManager.setTemperature(playerId, temp);
uiUpdater.updateTemperatureBlock(menu, playerId, temp);
}
-
- // Disable antiseptic protection if operation site becomes unclean
+
+ // Disable carbolic acid protection if operation site becomes unclean
if (!opSite.equals("Clean") && hasProtection) {
stateManager.setAntisepticProtection(playerId, false);
uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("protection-lost"));
}
-
- if (diagnosis != null) {
- runDiagnosisSpecificMechanics(player, menu, playerId, diagnosis);
+
+ // Complications only show once the surgeon knows what they are dealing with
+ Procedure procedure = stateManager.getProcedure(playerId);
+ if (procedure != null && stateManager.isExamined(playerId)) {
+ runComplications(player, menu, playerId, procedure);
}
}
-
+
// ==============================================
// Checks the patient is still online, in the same world, and within range
// Range gets a configurable multiplier so normal fidgeting doesn't end the surgery
@@ -219,269 +203,124 @@ private boolean isPatientPresent(Player surgeon) {
}
// ==============================================
- // Runs diagnosis-specific mechanics
+ // Runs the procedure's complications
// ==============================================
- private void runDiagnosisSpecificMechanics(Player player, Inventory menu, UUID playerId, String diagnosis) {
- switch (diagnosis) {
- case "Moldy Guts":
- // Forces bleeding somewhere between min and max moves since the last
- // sponge: 50/50 each move once min is reached, guaranteed at max
- int moldyGutsMin = plugin.getConfig().getInt("diagnosis-mechanics.moldy-guts.bleeding-interval-min", 3);
- int moldyGutsMax = plugin.getConfig().getInt("diagnosis-mechanics.moldy-guts.bleeding-interval-max", 4);
- int movesSinceSpongeNow = stateManager.getMovesSinceLastSponge(playerId);
- if (movesSinceSpongeNow >= moldyGutsMax
- || (movesSinceSpongeNow >= moldyGutsMin && ThreadLocalRandom.current().nextBoolean())) {
- stateManager.setBleeding(playerId, true);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("moldy-guts"));
- updateDynamicTools(player, menu, playerId);
- }
- break;
-
- case "Fatty Liver":
- double fattyLiverChance = plugin.getConfig().getDouble("diagnosis-mechanics.fatty-liver.heart-stop-chance", 0.20);
- if (stateManager.getStatus(playerId).equals("Unconscious") && ThreadLocalRandom.current().nextDouble() < fattyLiverChance) {
- stopHeart(player, menu, playerId, "fatty-liver-heart-stop");
- }
- break;
+ private void runComplications(Player player, Inventory menu, UUID playerId, Procedure procedure) {
+ if (procedure.has(Complication.HAEMORRHAGE)) {
+ // Forces bleeding somewhere between min and max moves since the last
+ // sponge: 50/50 each move once min is reached, guaranteed at max
+ int min = plugin.getConfig().getInt("complications.haemorrhage.bleeding-interval-min", 3);
+ int max = plugin.getConfig().getInt("complications.haemorrhage.bleeding-interval-max", 4);
+ int movesSinceSponge = stateManager.getMovesSinceLastSponge(playerId);
+ if (movesSinceSponge >= max || (movesSinceSponge >= min && ThreadLocalRandom.current().nextBoolean())) {
+ stateManager.setBleeding(playerId, true);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("haemorrhage"));
+ updateDynamicTools(player, menu, playerId);
+ }
+ }
- case "Broken Heart":
- double brokenHeartChance = plugin.getConfig().getDouble("diagnosis-mechanics.broken-heart.heart-stop-chance", 0.35);
- if (stateManager.getStatus(playerId).equals("Unconscious") && ThreadLocalRandom.current().nextDouble() < brokenHeartChance) {
- stopHeart(player, menu, playerId, "broken-heart-stop");
- }
- break;
-
- case "Arcane Infection":
- double arcaneChance = plugin.getConfig().getDouble("diagnosis-mechanics.arcane-infection.chaos-chance", 0.25);
- if (ThreadLocalRandom.current().nextDouble() < arcaneChance) {
- handleArcaneInfectionChaos(player, menu, playerId);
- }
- break;
-
- case "Lupus":
- double lupusChance = plugin.getConfig().getDouble("diagnosis-mechanics.lupus.howl-chance", 0.15);
- if (ThreadLocalRandom.current().nextDouble() < lupusChance) {
- int currentIncisions = stateManager.getIncisions(playerId);
- stateManager.setIncisions(playerId, currentIncisions + 1);
- uiUpdater.updateIncisionBlock(menu, playerId, currentIncisions + 1);
- stateManager.setBleeding(playerId, true);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("lupus-howl"));
- updateDynamicTools(player, menu, playerId);
- }
- break;
+ if (procedure.has(Complication.SHOCK)) {
+ double chance = plugin.getConfig().getDouble("complications.shock.collapse-chance", 0.20);
+ if (stateManager.getStatus(playerId).equals("Unconscious") && ThreadLocalRandom.current().nextDouble() < chance) {
+ collapse(player, menu, playerId);
+ }
}
}
-
+
// ==============================================
- // Stops the patient's heart: status, UI, message, death timer, tools
+ // The patient collapses: status, UI, message, death timer, tools
// ==============================================
- private void stopHeart(Player player, Inventory menu, UUID playerId, String messageKey) {
- stateManager.setStatus(playerId, "Heart Stopped");
- uiUpdater.updateStatusBlock(menu, playerId, "Heart Stopped");
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage(messageKey));
- int defibCountdown = plugin.getConfig().getInt("death-timers.defibrillator-countdown", 2);
- stateManager.setDefibrillatorCountdown(playerId, defibCountdown);
+ private void collapse(Player player, Inventory menu, UUID playerId) {
+ stateManager.setStatus(playerId, "Collapsed");
+ uiUpdater.updateStatusBlock(menu, playerId, "Collapsed");
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("shock-collapse"));
+ int countdown = plugin.getConfig().getInt("death-timers.collapse-countdown", 2);
+ stateManager.setCollapseCountdown(playerId, countdown);
updateDynamicTools(player, menu, playerId);
}
// ==============================================
- // Handles Arcane Infection chaos effects
+ // Shows the linen dressing once the procedure's incisions are made and
+ // every bone has been set
// ==============================================
- private void handleArcaneInfectionChaos(Player player, Inventory menu, UUID playerId) {
- int chaosEffect = ThreadLocalRandom.current().nextInt(4);
- double temp;
-
- switch (chaosEffect) {
- case 0: // Temperature spike
- double tempSpikeMax = plugin.getConfig().getDouble("diagnosis-mechanics.arcane-infection.temp-spike-max", 4.0);
- double maxTemp = plugin.getConfig().getDouble("temperature.instant-death-threshold", 110.0);
- temp = Math.min(stateManager.getTemperature(playerId) + ThreadLocalRandom.current().nextDouble() * tempSpikeMax, maxTemp);
- stateManager.setTemperature(playerId, temp);
- uiUpdater.updateTemperatureBlock(menu, playerId, temp);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("chaos-temp-spike"));
- break;
-
- case 1: // Temperature drop
- double tempDropMax = plugin.getConfig().getDouble("diagnosis-mechanics.arcane-infection.temp-drop-max", 2.0);
- double normalTemp = plugin.getConfig().getDouble("temperature.normal", 98.6);
- temp = Math.max(stateManager.getTemperature(playerId) - ThreadLocalRandom.current().nextDouble() * tempDropMax, normalTemp);
- stateManager.setTemperature(playerId, temp);
- uiUpdater.updateTemperatureBlock(menu, playerId, temp);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("chaos-temp-drop"));
- break;
-
- case 2: // Heart stop
- stopHeart(player, menu, playerId, "chaos-heart-stop");
- break;
-
- case 3: // Random status change
- String newStatus = SurgeryConstants.PATIENT_STATUSES[ThreadLocalRandom.current().nextInt(SurgeryConstants.PATIENT_STATUSES.length)];
- stateManager.setStatus(playerId, newStatus);
- uiUpdater.updateStatusBlock(menu, playerId, newStatus);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("chaos-status-change"));
- // A random "Heart Stopped" needs the death timer like every other
- // heart-stop path; without it the countdown stays null and the
- // patient can never die from the stopped heart
- if (newStatus.equals("Heart Stopped")) {
- int countdown = plugin.getConfig().getInt("death-timers.defibrillator-countdown", 2);
- stateManager.setDefibrillatorCountdown(playerId, countdown);
- } else {
- // Leaving "Heart Stopped" by chaos skips the defibrillator, so the
- // old countdown must not survive into a later heart stop
- stateManager.removeDefibrillatorCountdown(playerId);
- }
- // Align the anesthetic wear-off timer with the forced status, otherwise
- // a stale timer makes the patient wake instantly or far too late
- if (newStatus.equals("Unconscious")) {
- stateManager.setUnconsciousTimer(playerId, 0);
- } else if (newStatus.equals("Coming to")) {
- stateManager.setUnconsciousTimer(playerId, plugin.getConfig().getInt("anesthetic.unconscious-moves", 6));
- }
- updateDynamicTools(player, menu, playerId);
- break;
+ public void checkForDressing(Player player, Inventory menu, UUID playerId, int currentIncisions) {
+ Procedure procedure = stateManager.getProcedure(playerId);
+ if (procedure == null || !stateManager.isExamined(playerId) || stateManager.isCured(playerId)) {
+ return;
}
- }
-
- // ==============================================
- // Check if Fix it button should appear based on diagnosis and conditions
- // ==============================================
- public void checkForFixItButton(Player player, Inventory menu, UUID playerId, int currentIncisions) {
- String diagnosis = stateManager.getDiagnosis(playerId);
- if (diagnosis == null || stateManager.isCured(playerId)) {
+ if (currentIncisions < procedure.requiredIncisions()) {
return;
}
-
- // Check for flu diagnoses. Show surgical glove at normal temperature
- if (diagnosisChecker.isFlu(diagnosis)) {
- double currentTemp = stateManager.getTemperature(playerId);
- double normalTemp = plugin.getConfig().getDouble("temperature.normal", 98.6);
- if (Math.abs(currentTemp - normalTemp) < 0.1) {
- ItemStack surgicalGlove = api.getCreator().getItemFromPath(itemsConfig.getItemPath(5));
- if (surgicalGlove != null) {
- menu.setItem(SurgeryConstants.SLOT_SURGICAL_GLOVE, surgicalGlove);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("surgical-glove-ready"));
- }
- }
+ if (stateManager.getBrokenBones(playerId) > 0 || stateManager.getShatteredBones(playerId) > 0) {
return;
}
-
- // For non-flu diagnoses, check incision count
- if (requiredIncisions.containsKey(diagnosis)) {
- int requiredInc = requiredIncisions.get(diagnosis);
- if (currentIncisions >= requiredInc) {
- // If diagnosis has bones, check that all bones are fixed first
- if (diagnosisChecker.hasBones(diagnosis)) {
- int brokenBones = stateManager.getBrokenBones(playerId);
- int shatteredBones = stateManager.getShatteredBones(playerId);
- if (brokenBones > 0 || shatteredBones > 0) {
- return;
- }
- }
-
- ItemStack surgicalGlove = api.getCreator().getItemFromPath(itemsConfig.getItemPath(5));
- if (surgicalGlove != null) {
- menu.setItem(SurgeryConstants.SLOT_SURGICAL_GLOVE, surgicalGlove);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("surgical-glove-ready"));
- }
- }
+ if (menu.getItem(SurgeryTool.DRESSING.getSlot()) != null) {
+ return;
}
- }
-
- // ==============================================
- // Handles bone reveal logic for scalpel
- // ==============================================
- public void handleBoneReveal(Player player, Inventory menu, UUID playerId, String diagnosis, int incisions) {
- if (diagnosisChecker.hasBones(diagnosis)) {
- // >= so overshooting the required count still reveals; re-running is
- // idempotent because revealed counts track the actual bone counts
- int required = requiredIncisions.getOrDefault(diagnosis, 0);
- if (incisions >= required) {
- int totalBrokenBones = stateManager.getBrokenBones(playerId);
- int totalShatteredBones = stateManager.getShatteredBones(playerId);
- stateManager.setRevealedBrokenBones(playerId, totalBrokenBones);
- stateManager.setRevealedShatteredBones(playerId, totalShatteredBones);
- updateDynamicTools(player, menu, playerId);
- }
- } else {
- revealBonesRandomly(player, menu, playerId);
+ ItemStack dressing = api.getCreator().getItemFromPath(itemsConfig.getItemPath(SurgeryTool.DRESSING));
+ if (dressing != null) {
+ menu.setItem(SurgeryTool.DRESSING.getSlot(), dressing);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("dressing-ready"));
}
}
-
+
// ==============================================
- // Randomly reveal bones when scalpel is used (25% chance per incision)
+ // Reveals the procedure's bones once enough incisions expose them
// ==============================================
- private void revealBonesRandomly(Player player, Inventory menu, UUID playerId) {
- int totalBroken = stateManager.getBrokenBones(playerId);
- int totalShattered = stateManager.getShatteredBones(playerId);
- int revealedBroken = stateManager.getRevealedBrokenBones(playerId);
- int revealedShattered = stateManager.getRevealedShatteredBones(playerId);
-
- double revealChance = plugin.getConfig().getDouble("bones.random-reveal-chance", 0.25);
-
- if (revealedBroken < totalBroken && ThreadLocalRandom.current().nextDouble() < revealChance) {
- stateManager.setRevealedBrokenBones(playerId, revealedBroken + 1);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("discovered-broken-bone"));
+ public void handleBoneReveal(Player player, Inventory menu, UUID playerId, int incisions) {
+ Procedure procedure = stateManager.getProcedure(playerId);
+ if (procedure == null || !procedure.hasBones() || !stateManager.isExamined(playerId)) {
+ return;
}
-
- if (revealedShattered < totalShattered && ThreadLocalRandom.current().nextDouble() < revealChance) {
- stateManager.setRevealedShatteredBones(playerId, revealedShattered + 1);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("discovered-shattered-bone"));
+ // >= so overshooting the required count still reveals; re-running is
+ // idempotent because revealed counts track the actual bone counts
+ if (incisions >= procedure.requiredIncisions()) {
+ int brokenBefore = stateManager.getRevealedBrokenBones(playerId);
+ int shatteredBefore = stateManager.getRevealedShatteredBones(playerId);
+ stateManager.setRevealedBrokenBones(playerId, stateManager.getBrokenBones(playerId));
+ stateManager.setRevealedShatteredBones(playerId, stateManager.getShatteredBones(playerId));
+ if (stateManager.getRevealedBrokenBones(playerId) > brokenBefore) {
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("discovered-broken-bone"));
+ }
+ if (stateManager.getRevealedShatteredBones(playerId) > shatteredBefore) {
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("discovered-shattered-bone"));
+ }
+ uiUpdater.updateDiagnosisBlock(menu, playerId);
+ updateDynamicTools(player, menu, playerId);
}
-
- updateDynamicTools(player, menu, playerId);
}
-
+
// ==============================================
// Updates dynamic tools that appear based on conditions
// ==============================================
public void updateDynamicTools(Player player, Inventory menu, UUID playerId) {
- // Defibrillator: appears when heart stopped
- String status = stateManager.getStatus(playerId);
- if (status.equals("Heart Stopped") && menu.getItem(SurgeryConstants.SLOT_DEFIBRILLATOR) == null) {
- ItemStack defibrillator = api.getCreator().getItemFromPath(itemsConfig.getItemPath(9));
- if (defibrillator != null) {
- menu.setItem(SurgeryConstants.SLOT_DEFIBRILLATOR, defibrillator);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("defibrillator-available"));
- }
- } else if (!status.equals("Heart Stopped") && menu.getItem(SurgeryConstants.SLOT_DEFIBRILLATOR) != null) {
- menu.setItem(SurgeryConstants.SLOT_DEFIBRILLATOR, null);
- }
+ // Smelling salts: appear when the patient has collapsed
+ boolean collapsed = stateManager.getStatus(playerId).equals("Collapsed");
+ showTool(player, menu, SurgeryTool.SMELLING_SALTS, collapsed, "smelling-salts-available");
- // Pins: appears when shattered bones revealed
- int revealedShattered = stateManager.getRevealedShatteredBones(playerId);
- if (revealedShattered > 0 && menu.getItem(SurgeryConstants.SLOT_PINS) == null) {
- ItemStack pins = api.getCreator().getItemFromPath(itemsConfig.getItemPath(10));
- if (pins != null) {
- menu.setItem(SurgeryConstants.SLOT_PINS, pins);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("pins-available"));
- }
- } else if (revealedShattered == 0 && menu.getItem(SurgeryConstants.SLOT_PINS) != null) {
- menu.setItem(SurgeryConstants.SLOT_PINS, null);
- }
+ // Silver wire: appears when shattered bones are revealed
+ showTool(player, menu, SurgeryTool.SILVER_WIRE, stateManager.getRevealedShatteredBones(playerId) > 0,
+ "silver-wire-available");
- // Splint: appears when broken bones revealed
- int revealedBroken = stateManager.getRevealedBrokenBones(playerId);
- if (revealedBroken > 0 && menu.getItem(SurgeryConstants.SLOT_SPLINT) == null) {
- ItemStack splint = api.getCreator().getItemFromPath(itemsConfig.getItemPath(11));
- if (splint != null) {
- menu.setItem(SurgeryConstants.SLOT_SPLINT, splint);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("splint-available"));
- }
- } else if (revealedBroken == 0 && menu.getItem(SurgeryConstants.SLOT_SPLINT) != null) {
- menu.setItem(SurgeryConstants.SLOT_SPLINT, null);
- }
+ // Splint: appears when broken bones are revealed
+ showTool(player, menu, SurgeryTool.SPLINT, stateManager.getRevealedBrokenBones(playerId) > 0,
+ "splint-available");
- // Clamp: appears when incisions > 1 AND bleeding
- int incisions = stateManager.getIncisions(playerId);
- boolean bleeding = stateManager.isBleeding(playerId);
- if (incisions > 1 && bleeding && menu.getItem(SurgeryConstants.SLOT_CLAMP) == null) {
- ItemStack clamp = api.getCreator().getItemFromPath(itemsConfig.getItemPath(12));
- if (clamp != null) {
- menu.setItem(SurgeryConstants.SLOT_CLAMP, clamp);
- uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage("clamp-available"));
+ // Artery forceps: appear when incisions > 1 AND bleeding
+ boolean forceps = stateManager.getIncisions(playerId) > 1 && stateManager.isBleeding(playerId);
+ showTool(player, menu, SurgeryTool.ARTERY_FORCEPS, forceps, "artery-forceps-available");
+ }
+
+ private void showTool(Player player, Inventory menu, SurgeryTool tool, boolean visible, String messageKey) {
+ boolean shown = menu.getItem(tool.getSlot()) != null;
+ if (visible && !shown) {
+ ItemStack item = api.getCreator().getItemFromPath(itemsConfig.getItemPath(tool));
+ if (item != null) {
+ menu.setItem(tool.getSlot(), item);
+ uiUpdater.sendNumberedMessage(player, uiUpdater.getMessage(messageKey));
}
- } else if ((incisions <= 1 || !bleeding) && menu.getItem(SurgeryConstants.SLOT_CLAMP) != null) {
- menu.setItem(SurgeryConstants.SLOT_CLAMP, null);
+ } else if (!visible && shown) {
+ menu.setItem(tool.getSlot(), null);
}
}
}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuBuilder.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuBuilder.java
index 4756708..2f4f4e0 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuBuilder.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuBuilder.java
@@ -36,38 +36,23 @@ public SurgeryMenuBuilder(JavaPlugin plugin, ItemAPI api, SurgeryStateManager st
public void buildAndOpenMenu(Player player) {
Inventory menu = new SurgeryMenuHolder().getInventory();
- // Map specific slots to item config indexes
- int[][] slotMapping = {
- {SurgeryConstants.SLOT_SPONGE, 0},
- {SurgeryConstants.SLOT_SCALPEL, 1},
- {SurgeryConstants.SLOT_STITCHES, 2},
- {SurgeryConstants.SLOT_ANTISEPTIC, 4},
- {SurgeryConstants.SLOT_ULTRASOUND, 6},
- {SurgeryConstants.SLOT_LAB_KIT, 7},
- {SurgeryConstants.SLOT_ANESTHETIC, 8},
- {SurgeryConstants.SLOT_TRANSFUSION, 13}
-
- // Antibiotics appear after using the lab kit
- // Surgical Glove, Defibrillator, Pins, Splint, and Clamp
- // appear dynamically
+ // Tools on the table from the start; the tincture appears after the
+ // thermometer, and the dressing, smelling salts, silver wire, splint,
+ // and artery forceps appear when needed
+ SurgeryTool[] startingTools = {
+ SurgeryTool.SPONGE, SurgeryTool.SCALPEL, SurgeryTool.SUTURE, SurgeryTool.CARBOLIC_ACID,
+ SurgeryTool.STETHOSCOPE, SurgeryTool.THERMOMETER, SurgeryTool.CHLOROFORM, SurgeryTool.TRANSFUSION
};
-
- // Place items in the specified slots
- for (int[] mapping : slotMapping) {
- int slot = mapping[0];
- int itemIndex = mapping[1];
-
- String itemPath = itemsConfig.getItemPath(itemIndex);
- if (itemPath != null) {
- ItemStack item = api.getCreator().getItemFromPath(itemPath);
- if (item != null) {
- menu.setItem(slot, item);
- } else {
- plugin.getLogger().warning("[Surgery] Could not load item: " + itemPath);
- }
+ for (SurgeryTool tool : startingTools) {
+ String itemPath = itemsConfig.getItemPath(tool);
+ ItemStack item = itemPath == null ? null : api.getCreator().getItemFromPath(itemPath);
+ if (item != null) {
+ menu.setItem(tool.getSlot(), item);
+ } else {
+ plugin.getLogger().warning("[Surgery] Could not load item: " + itemPath);
}
}
-
+
// Add placeholder info blocks
for (int i = SurgeryConstants.INFO_SLOT_FIRST; i <= SurgeryConstants.INFO_SLOT_LAST; i++) {
ItemStack infoBlock = uiUpdater.createInfoBlock(Material.RED_CONCRETE, " ", "");
@@ -89,8 +74,7 @@ private void initializePlayerState(Player player, Inventory menu) {
// ==============================================
// Set the first info block (slot 10) as "diagnosis"
// ==============================================
- ItemStack diagnosisBlock = uiUpdater.createInfoBlock(Material.RED_CONCRETE, ChatColor.GOLD + "Diagnosis", ChatColor.GRAY + "The patient has not been diagnosed.");
- menu.setItem(SurgeryConstants.SLOT_DIAGNOSIS, diagnosisBlock);
+ uiUpdater.updateDiagnosisBlock(menu, playerId);
// ==============================================
// Set the second info block (slot 11) as "pulse". Always starts at Strong
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java
index fd727e8..ae8a618 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryMenuManager.java
@@ -1,5 +1,7 @@
package net.tfminecraft.surgery.managers;
+import net.tfminecraft.rpcharacters.api.HealingInjuries.HealingInjury;
+import net.tfminecraft.surgery.procedures.ProcedureRegistry;
import net.tfminecraft.tlibs.objects.api.ItemAPI;
import net.tfminecraft.tlibs.TLibs;
import org.bukkit.entity.Player;
@@ -17,7 +19,8 @@ public class SurgeryMenuManager {
private ItemAPI api;
// Specialized managers
- private DiagnosisChecker diagnosisChecker;
+ private final ProcedureRegistry procedures = new ProcedureRegistry();
+ private final SurgeryRequestManager requestManager = new SurgeryRequestManager();
private SurgeryStateManager stateManager;
private SurgeryUIUpdater uiUpdater;
private SurgeryMenuBuilder menuBuilder;
@@ -43,27 +46,28 @@ public void initialize() {
}
// Initialize all managers in dependency order
- diagnosisChecker = new DiagnosisChecker(plugin);
+ procedures.load(plugin.getConfig(), plugin.getLogger()::warning);
stateManager = new SurgeryStateManager();
- uiUpdater = new SurgeryUIUpdater(plugin, stateManager, diagnosisChecker);
+ uiUpdater = new SurgeryUIUpdater(plugin, stateManager);
completionHandler = new SurgeryCompletionHandler(plugin, stateManager, uiUpdater);
- mechanicsManager = new SurgeryMechanicsManager(plugin, api, stateManager, uiUpdater, completionHandler, diagnosisChecker, itemsConfig);
+ mechanicsManager = new SurgeryMechanicsManager(plugin, api, stateManager, uiUpdater, completionHandler, itemsConfig);
menuBuilder = new SurgeryMenuBuilder(plugin, api, stateManager, uiUpdater, itemsConfig);
- itemHandler = new SurgeryItemHandler(plugin, api, stateManager, uiUpdater, mechanicsManager, completionHandler, diagnosisChecker, itemsConfig);
+ itemHandler = new SurgeryItemHandler(plugin, api, stateManager, uiUpdater, mechanicsManager, completionHandler, itemsConfig);
// Initialize any managers that need config
- mechanicsManager.initialize();
itemHandler.initialize();
plugin.getLogger().info("[Surgery] Surgery menu manager initialized!");
}
// ==============================================
- // Opens the surgery menu for the surgeon, operating on the specified patient
+ // Opens the surgery menu for the surgeon, operating on the patient's ailment
// ==============================================
- public void openSurgeryMenu(Player surgeon, Player patient) {
+ public void openSurgeryMenu(Player surgeon, Player patient, HealingInjury ailment) {
stateManager.setPatientName(surgeon.getUniqueId(), patient.getName());
stateManager.setPatientUuid(surgeon.getUniqueId(), patient.getUniqueId());
+ stateManager.setAilment(surgeon.getUniqueId(), ailment.traitId(), ailment.displayName(),
+ procedures.get(ailment.traitId()));
menuBuilder.buildAndOpenMenu(surgeon);
}
@@ -121,6 +125,7 @@ public void handlePatientQuit(Player patient) {
// Getters for accessing individual managers
// ==============================================
public SurgeryStateManager getStateManager() { return stateManager; }
+ public SurgeryRequestManager getRequestManager() { return requestManager; }
public SurgeryUIUpdater getUiUpdater() { return uiUpdater; }
public SurgeryMenuBuilder getMenuBuilder() { return menuBuilder; }
public SurgeryCompletionHandler getCompletionHandler() { return completionHandler; }
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java
new file mode 100644
index 0000000..6e41626
--- /dev/null
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java
@@ -0,0 +1,37 @@
+package net.tfminecraft.surgery.managers;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+// ==============================================
+// Pending surgery offers, keyed by patient. A failed surgery lengthens the
+// patient's healing time, so nobody is operated on without agreeing to it
+// ==============================================
+public class SurgeryRequestManager {
+
+ public record Request(UUID surgeonId, String traitId, long expiresAt) {
+ }
+
+ private final Map requests = new HashMap<>();
+
+ public void offer(UUID patientId, UUID surgeonId, String traitId, long expiresAt) {
+ requests.put(patientId, new Request(surgeonId, traitId, expiresAt));
+ }
+
+ // ==============================================
+ // Removes and returns the patient's request if it has not expired
+ // ==============================================
+ public Request take(UUID patientId, long now) {
+ Request request = requests.remove(patientId);
+ return request == null || request.expiresAt() < now ? null : request;
+ }
+
+ // ==============================================
+ // Drops every request the player sent or received (used on quit)
+ // ==============================================
+ public void forget(UUID playerId) {
+ requests.remove(playerId);
+ requests.values().removeIf(request -> request.surgeonId().equals(playerId));
+ }
+}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryStateManager.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryStateManager.java
index 6054f48..11d1130 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryStateManager.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryStateManager.java
@@ -1,5 +1,7 @@
package net.tfminecraft.surgery.managers;
+import net.tfminecraft.surgery.procedures.Procedure;
+
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@@ -17,7 +19,12 @@ public class SurgeryStateManager {
private static class SurgerySession {
String patientName;
UUID patientUuid;
- String diagnosis;
+ String traitId;
+ String ailmentName;
+ Procedure procedure;
+ Boolean examined;
+ // Treatment has begun: the patient was sedated or cut
+ Boolean operated;
String pulse;
String status;
Double temperature;
@@ -29,7 +36,7 @@ private static class SurgerySession {
Integer shatteredBones;
Integer revealedBrokenBones;
Integer revealedShatteredBones;
- Integer defibrillatorCountdown;
+ Integer collapseCountdown;
Boolean cured;
Boolean antisepticProtection;
Boolean spongeEffect;
@@ -54,7 +61,11 @@ private SurgerySession get(UUID playerId) {
// ==============================================
// Getters with default values
// ==============================================
- public String getDiagnosis(UUID playerId) { SurgerySession s = get(playerId); return s == null ? null : s.diagnosis; }
+ public String getTraitId(UUID playerId) { SurgerySession s = get(playerId); return s == null ? null : s.traitId; }
+ public String getAilmentName(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.ailmentName == null ? "Unknown ailment" : s.ailmentName; }
+ public Procedure getProcedure(UUID playerId) { SurgerySession s = get(playerId); return s == null ? null : s.procedure; }
+ public boolean isExamined(UUID playerId) { SurgerySession s = get(playerId); return s != null && Boolean.TRUE.equals(s.examined); }
+ public boolean hasOperated(UUID playerId) { SurgerySession s = get(playerId); return s != null && Boolean.TRUE.equals(s.operated); }
public String getPulse(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.pulse == null ? "Strong" : s.pulse; }
public String getStatus(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.status == null ? "Awake" : s.status; }
public double getTemperature(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.temperature == null ? 98.6 : s.temperature; }
@@ -66,7 +77,7 @@ private SurgerySession get(UUID playerId) {
public int getShatteredBones(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.shatteredBones == null ? 0 : s.shatteredBones; }
public int getRevealedBrokenBones(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.revealedBrokenBones == null ? 0 : s.revealedBrokenBones; }
public int getRevealedShatteredBones(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.revealedShatteredBones == null ? 0 : s.revealedShatteredBones; }
- public Integer getDefibrillatorCountdown(UUID playerId) { SurgerySession s = get(playerId); return s == null ? null : s.defibrillatorCountdown; }
+ public Integer getCollapseCountdown(UUID playerId) { SurgerySession s = get(playerId); return s == null ? null : s.collapseCountdown; }
public boolean isCured(UUID playerId) { SurgerySession s = get(playerId); return s != null && Boolean.TRUE.equals(s.cured); }
public boolean hasAntisepticProtection(UUID playerId) { SurgerySession s = get(playerId); return s != null && Boolean.TRUE.equals(s.antisepticProtection); }
public boolean hasSpongeEffect(UUID playerId) { SurgerySession s = get(playerId); return s != null && Boolean.TRUE.equals(s.spongeEffect); }
@@ -79,7 +90,6 @@ private SurgerySession get(UUID playerId) {
public String getPatientName(UUID playerId) { SurgerySession s = get(playerId); return s == null || s.patientName == null ? "Unknown" : s.patientName; }
public UUID getPatientUuid(UUID playerId) { SurgerySession s = get(playerId); return s == null ? null : s.patientUuid; }
public boolean hasSession(UUID playerId) { return sessions.containsKey(playerId); }
- public boolean hasDiagnosis(UUID playerId) { SurgerySession s = get(playerId); return s != null && s.diagnosis != null; }
// ==============================================
// Patient-side lookups (sessions are keyed by surgeon)
@@ -100,7 +110,14 @@ public UUID findSurgeonForPatient(UUID patientUuid) {
// ==============================================
// Setters
// ==============================================
- public void setDiagnosis(UUID playerId, String diagnosis) { session(playerId).diagnosis = diagnosis; }
+ public void setAilment(UUID playerId, String traitId, String ailmentName, Procedure procedure) {
+ SurgerySession s = session(playerId);
+ s.traitId = traitId;
+ s.ailmentName = ailmentName;
+ s.procedure = procedure;
+ }
+ public void setExamined(UUID playerId, boolean examined) { session(playerId).examined = examined; }
+ public void setOperated(UUID playerId, boolean operated) { session(playerId).operated = operated; }
public void setPulse(UUID playerId, String pulse) { session(playerId).pulse = pulse; }
public void setStatus(UUID playerId, String status) { session(playerId).status = status; }
public void setTemperature(UUID playerId, double temp) { session(playerId).temperature = temp; }
@@ -112,7 +129,7 @@ public UUID findSurgeonForPatient(UUID patientUuid) {
public void setShatteredBones(UUID playerId, int count) { session(playerId).shatteredBones = count; }
public void setRevealedBrokenBones(UUID playerId, int count) { session(playerId).revealedBrokenBones = count; }
public void setRevealedShatteredBones(UUID playerId, int count) { session(playerId).revealedShatteredBones = count; }
- public void setDefibrillatorCountdown(UUID playerId, int countdown) { session(playerId).defibrillatorCountdown = countdown; }
+ public void setCollapseCountdown(UUID playerId, int countdown) { session(playerId).collapseCountdown = countdown; }
public void setCured(UUID playerId, boolean cured) { session(playerId).cured = cured; }
public void setAntisepticProtection(UUID playerId, boolean protected_) { session(playerId).antisepticProtection = protected_; }
public void setSpongeEffect(UUID playerId, boolean effect) { session(playerId).spongeEffect = effect; }
@@ -124,7 +141,7 @@ public UUID findSurgeonForPatient(UUID patientUuid) {
public void setRedTempCounter(UUID playerId, int count) { session(playerId).redTempCounter = count; }
public void setPatientName(UUID playerId, String name) { session(playerId).patientName = name; }
public void setPatientUuid(UUID playerId, UUID patientUuid) { session(playerId).patientUuid = patientUuid; }
- public void removeDefibrillatorCountdown(UUID playerId) { SurgerySession s = get(playerId); if (s != null) { s.defibrillatorCountdown = null; } }
+ public void removeCollapseCountdown(UUID playerId) { SurgerySession s = get(playerId); if (s != null) { s.collapseCountdown = null; } }
// ==============================================
// Player Data Cleanup
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryTool.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryTool.java
new file mode 100644
index 0000000..aed5a59
--- /dev/null
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryTool.java
@@ -0,0 +1,49 @@
+package net.tfminecraft.surgery.managers;
+
+// ==============================================
+// Victorian surgical instruments: config key, default TLibs item path, and menu slot
+// Default paths keep the original MMOItems IDs so existing crafted tools keep working
+// ==============================================
+public enum SurgeryTool {
+
+ SPONGE("sponge", "m.surgery.sponge", 28),
+ SCALPEL("scalpel", "m.surgery.scalpel", 29),
+ SUTURE("suture", "m.surgery.stitches", 30),
+ TINCTURE("tincture", "m.surgery.antibiotics", 31),
+ CARBOLIC_ACID("carbolic-acid", "m.surgery.antiseptic", 32),
+ DRESSING("dressing", "m.surgery.surgical_glove", 33),
+ STETHOSCOPE("stethoscope", "m.surgery.ultrasound", 34),
+ THERMOMETER("thermometer", "m.surgery.lab_kit", 37),
+ CHLOROFORM("chloroform", "m.surgery.anesthetic", 38),
+ SMELLING_SALTS("smelling-salts", "m.surgery.defibrillator", 39),
+ SILVER_WIRE("silver-wire", "m.surgery.pins", 40),
+ SPLINT("splint", "m.surgery.splint", 41),
+ ARTERY_FORCEPS("artery-forceps", "m.surgery.clamp", 42),
+ TRANSFUSION("transfusion", "m.surgery.transfusion", 43);
+
+ private final String configKey;
+ private final String defaultPath;
+ private final int slot;
+
+ SurgeryTool(String configKey, String defaultPath, int slot) {
+ this.configKey = configKey;
+ this.defaultPath = defaultPath;
+ this.slot = slot;
+ }
+
+ public String getConfigKey() { return configKey; }
+ public String getDefaultPath() { return defaultPath; }
+ public int getSlot() { return slot; }
+
+ // ==============================================
+ // Finds the tool shown in a menu slot, or null for info blocks and empty slots
+ // ==============================================
+ public static SurgeryTool fromSlot(int slot) {
+ for (SurgeryTool tool : values()) {
+ if (tool.slot == slot) {
+ return tool;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryUIUpdater.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryUIUpdater.java
index b6c25c9..1556aeb 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryUIUpdater.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryUIUpdater.java
@@ -1,5 +1,6 @@
package net.tfminecraft.surgery.managers;
+import net.tfminecraft.surgery.procedures.Procedure;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
@@ -20,13 +21,11 @@ public class SurgeryUIUpdater {
private final JavaPlugin plugin;
private final SurgeryStateManager stateManager;
- private final DiagnosisChecker diagnosisChecker;
private FileConfiguration messages;
- public SurgeryUIUpdater(JavaPlugin plugin, SurgeryStateManager stateManager, DiagnosisChecker diagnosisChecker) {
+ public SurgeryUIUpdater(JavaPlugin plugin, SurgeryStateManager stateManager) {
this.plugin = plugin;
this.stateManager = stateManager;
- this.diagnosisChecker = diagnosisChecker;
loadMessages();
}
@@ -131,37 +130,37 @@ public void updatePulseBlock(Inventory menu, UUID playerId, String pulse) {
// ==============================================
// Updates the diagnosis block
+ // Before the stethoscope examination only the patient's complaint is known
// ==============================================
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
public void updateDiagnosisBlock(Inventory menu, UUID playerId) {
- String diagnosis = stateManager.getDiagnosis(playerId);
- boolean cured = stateManager.isCured(playerId);
-
- Material diagnosisColor = cured ? Material.LIME_CONCRETE : Material.RED_CONCRETE;
- String diagnosisText = diagnosis != null ? ChatColor.GRAY + diagnosis : ChatColor.GRAY + "The patient has not been diagnosed.";
-
- ItemStack diagnosisBlock;
- if (diagnosis != null && diagnosisChecker.hasBones(diagnosis)) {
- // ==============================================
- // Show bone information in description
- // ==============================================
- int brokenBones = stateManager.getRevealedBrokenBones(playerId);
- int shatteredBones = stateManager.getRevealedShatteredBones(playerId);
-
- java.util.List lore = new java.util.ArrayList<>();
- lore.add(diagnosisText);
- lore.add(ChatColor.YELLOW + "Broken Bones: " + ChatColor.GRAY + brokenBones);
- lore.add(ChatColor.RED + "Shattered Bones: " + ChatColor.GRAY + shatteredBones);
-
- diagnosisBlock = createInfoBlock(diagnosisColor, ChatColor.GOLD + "Diagnosis", lore);
+ String ailment = stateManager.getAilmentName(playerId);
+ java.util.List lore = new java.util.ArrayList<>();
+ Material color;
+
+ Procedure procedure = stateManager.getProcedure(playerId);
+ if (!stateManager.isExamined(playerId) || procedure == null) {
+ color = Material.RED_CONCRETE;
+ lore.add(ChatColor.GRAY + "Complaint: " + ChatColor.WHITE + ailment);
+ lore.add(ChatColor.GRAY + "Examine the patient with the stethoscope.");
} else {
- diagnosisBlock = createInfoBlock(diagnosisColor, ChatColor.GOLD + "Diagnosis", diagnosisText);
+ color = stateManager.isCured(playerId) ? Material.LIME_CONCRETE : Material.YELLOW_CONCRETE;
+ lore.add(ChatColor.GRAY + "The patient suffers from " + ChatColor.WHITE + procedure.name());
+ lore.add(ChatColor.GRAY + "Ailment: " + ChatColor.WHITE + ailment);
+ lore.add(ChatColor.GRAY + "Incisions needed: " + ChatColor.WHITE + procedure.requiredIncisions());
+ if (procedure.hasBones()) {
+ lore.add(ChatColor.YELLOW + "Broken Bones: " + ChatColor.GRAY + stateManager.getRevealedBrokenBones(playerId));
+ lore.add(ChatColor.RED + "Shattered Bones: " + ChatColor.GRAY + stateManager.getRevealedShatteredBones(playerId));
+ }
+ if (stateManager.isCured(playerId)) {
+ lore.add(ChatColor.GREEN + "Treated and dressed");
+ }
}
-
- menu.setItem(SurgeryConstants.SLOT_DIAGNOSIS, diagnosisBlock);
+
+ menu.setItem(SurgeryConstants.SLOT_DIAGNOSIS, createInfoBlock(color, ChatColor.GOLD + "Diagnosis", lore));
}
-
+
// ==============================================
// Sends a numbered message to the player
// ==============================================
diff --git a/src/main/java/net/tfminecraft/surgery/procedures/Ailments.java b/src/main/java/net/tfminecraft/surgery/procedures/Ailments.java
new file mode 100644
index 0000000..d626ef5
--- /dev/null
+++ b/src/main/java/net/tfminecraft/surgery/procedures/Ailments.java
@@ -0,0 +1,44 @@
+package net.tfminecraft.surgery.procedures;
+
+import net.tfminecraft.rpcharacters.api.HealingInjuries;
+import net.tfminecraft.rpcharacters.api.HealingInjuries.HealingInjury;
+import org.bukkit.entity.Player;
+
+import java.util.Comparator;
+import java.util.List;
+
+// ==============================================
+// Reads and changes a patient's healing ailments through RPCharacters
+// Permanent injuries are never returned, so surgery cannot touch them
+// ==============================================
+public final class Ailments {
+
+ private Ailments() {
+ }
+
+ // ==============================================
+ // The ailment surgery treats next: the one with the most healing time left
+ // ==============================================
+ public static HealingInjury mostSevere(Player patient) {
+ List injuries = HealingInjuries.list(patient);
+ return injuries.stream().max(Comparator.comparingLong(HealingInjury::remainingMs)).orElse(null);
+ }
+
+ public static HealingInjury find(Player patient, String traitId) {
+ for (HealingInjury injury : HealingInjuries.list(patient)) {
+ if (injury.traitId().equalsIgnoreCase(traitId)) {
+ return injury;
+ }
+ }
+ return null;
+ }
+
+ public static boolean cure(Player patient, String traitId) {
+ return HealingInjuries.cure(patient, traitId);
+ }
+
+ // Returns the new remaining time, or -1 if the ailment is gone
+ public static long extend(Player patient, String traitId, long extraMs) {
+ return HealingInjuries.extend(patient, traitId, extraMs);
+ }
+}
diff --git a/src/main/java/net/tfminecraft/surgery/procedures/Complication.java b/src/main/java/net/tfminecraft/surgery/procedures/Complication.java
new file mode 100644
index 0000000..14ea93a
--- /dev/null
+++ b/src/main/java/net/tfminecraft/surgery/procedures/Complication.java
@@ -0,0 +1,27 @@
+package net.tfminecraft.surgery.procedures;
+
+import java.util.Locale;
+
+// ==============================================
+// Complications a procedure can carry, each adding pressure during the operation
+// ==============================================
+public enum Complication {
+
+ // Wound reopens and bleeds every few moves unless sponged
+ HAEMORRHAGE,
+ // Patient may collapse while under chloroform and need smelling salts
+ SHOCK,
+ // Patient arrives feverish and the fever keeps climbing
+ SEPSIS;
+
+ public static Complication fromConfig(String value) {
+ if (value == null) {
+ return null;
+ }
+ try {
+ return valueOf(value.trim().toUpperCase(Locale.ROOT).replace('-', '_'));
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/net/tfminecraft/surgery/procedures/Durations.java b/src/main/java/net/tfminecraft/surgery/procedures/Durations.java
new file mode 100644
index 0000000..ada9ad7
--- /dev/null
+++ b/src/main/java/net/tfminecraft/surgery/procedures/Durations.java
@@ -0,0 +1,51 @@
+package net.tfminecraft.surgery.procedures;
+
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+// ==============================================
+// Parses config durations such as "12h", "90m" or "1d 6h" into milliseconds
+// ==============================================
+public final class Durations {
+
+ private static final Pattern PART = Pattern.compile("(\\d+)\\s*([dhms])");
+
+ private Durations() {
+ }
+
+ // Returns -1 when the value is blank or not a valid duration
+ public static long parseMs(String value) {
+ if (value == null || value.isBlank()) {
+ return -1L;
+ }
+ String normalized = value.trim().toLowerCase(Locale.ROOT);
+ Matcher matcher = PART.matcher(normalized);
+ long total = 0L;
+ int end = 0;
+ while (matcher.find()) {
+ if (!normalized.substring(end, matcher.start()).isBlank()) {
+ return -1L;
+ }
+ long amount = Long.parseLong(matcher.group(1));
+ total += switch (matcher.group(2)) {
+ case "d" -> amount * 86_400_000L;
+ case "h" -> amount * 3_600_000L;
+ case "m" -> amount * 60_000L;
+ default -> amount * 1_000L;
+ };
+ end = matcher.end();
+ }
+ return end > 0 && normalized.substring(end).isBlank() ? total : -1L;
+ }
+
+ // ==============================================
+ // Short hours form for player messages: "12h", or "45m" under an hour
+ // ==============================================
+ public static String formatHours(long ms) {
+ if (ms < 3_600_000L) {
+ return Math.max(0L, (ms + 59_999L) / 60_000L) + "m";
+ }
+ return ((ms + 3_599_999L) / 3_600_000L) + "h";
+ }
+}
diff --git a/src/main/java/net/tfminecraft/surgery/procedures/Procedure.java b/src/main/java/net/tfminecraft/surgery/procedures/Procedure.java
new file mode 100644
index 0000000..f851dae
--- /dev/null
+++ b/src/main/java/net/tfminecraft/surgery/procedures/Procedure.java
@@ -0,0 +1,22 @@
+package net.tfminecraft.surgery.procedures;
+
+import java.util.Set;
+
+// ==============================================
+// How a healing ailment is operated on
+// ==============================================
+public record Procedure(String name, int requiredIncisions, int brokenBones, int shatteredBones,
+ Set complications) {
+
+ public Procedure {
+ complications = Set.copyOf(complications);
+ }
+
+ public boolean hasBones() {
+ return brokenBones > 0 || shatteredBones > 0;
+ }
+
+ public boolean has(Complication complication) {
+ return complications.contains(complication);
+ }
+}
diff --git a/src/main/java/net/tfminecraft/surgery/procedures/ProcedureRegistry.java b/src/main/java/net/tfminecraft/surgery/procedures/ProcedureRegistry.java
new file mode 100644
index 0000000..3b10304
--- /dev/null
+++ b/src/main/java/net/tfminecraft/surgery/procedures/ProcedureRegistry.java
@@ -0,0 +1,71 @@
+package net.tfminecraft.surgery.procedures;
+
+import org.bukkit.configuration.ConfigurationSection;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+// ==============================================
+// Procedures keyed by RPCharacters healing trait ID, with a fallback for
+// healing ailments that have no procedure of their own
+// ==============================================
+public class ProcedureRegistry {
+
+ private static final Procedure BUILT_IN_DEFAULT = new Procedure("Injury", 2, 0, 0, Set.of());
+
+ private final Map procedures = new HashMap<>();
+ private Procedure defaultProcedure = BUILT_IN_DEFAULT;
+
+ // ==============================================
+ // Loads "procedures" and "default-procedure" from the plugin config
+ // Unknown complication names are reported through the warning callback
+ // ==============================================
+ public void load(ConfigurationSection root, Consumer warning) {
+ procedures.clear();
+ ConfigurationSection defaults = root.getConfigurationSection("default-procedure");
+ defaultProcedure = defaults == null ? BUILT_IN_DEFAULT : parse(defaults, BUILT_IN_DEFAULT, warning);
+
+ ConfigurationSection section = root.getConfigurationSection("procedures");
+ if (section == null) {
+ return;
+ }
+ for (String traitId : section.getKeys(false)) {
+ ConfigurationSection procedure = section.getConfigurationSection(traitId);
+ if (procedure != null) {
+ procedures.put(traitId.toLowerCase(Locale.ROOT), parse(procedure, defaultProcedure, warning));
+ }
+ }
+ }
+
+ // ==============================================
+ // The procedure for a healing trait ID, falling back to the default procedure
+ // ==============================================
+ public Procedure get(String traitId) {
+ if (traitId == null) {
+ return defaultProcedure;
+ }
+ return procedures.getOrDefault(traitId.toLowerCase(Locale.ROOT), defaultProcedure);
+ }
+
+ private static Procedure parse(ConfigurationSection section, Procedure fallback, Consumer warning) {
+ Set complications = EnumSet.noneOf(Complication.class);
+ for (String value : section.getStringList("complications")) {
+ Complication complication = Complication.fromConfig(value);
+ if (complication == null) {
+ warning.accept("Unknown complication '" + value + "' in " + section.getCurrentPath());
+ } else {
+ complications.add(complication);
+ }
+ }
+ return new Procedure(
+ section.getString("name", fallback.name()),
+ Math.max(0, section.getInt("required-incisions", fallback.requiredIncisions())),
+ Math.max(0, section.getInt("broken-bones", fallback.brokenBones())),
+ Math.max(0, section.getInt("shattered-bones", fallback.shatteredBones())),
+ complications);
+ }
+}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 0dba8b1..f15f76a 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,5 +1,19 @@
# Surgery Plugin Configuration
-# This file contains all gameplay settings and mechanics
+# Physicians operate on the healing ailments of RPCharacters characters
+# (broken arm, broken leg, half blind, ...). Permanent injuries cannot be
+# treated. A successful surgery cures the ailment; a failed one makes it take
+# longer to heal.
+
+# Permission needed to operate. The Physician profession upgrade grants it,
+# the same permission that unlocks crafting the surgical tools
+permission: "professions.physician"
+
+# Healing time added to the ailment when a surgery fails after the patient
+# was sedated or cut (d/h/m/s, e.g. "12h" or "1d 6h"; "0h" disables it)
+failure-healing-penalty: "12h"
+
+# How long a patient has to accept a surgery offer
+request-timeout-seconds: 60
# Maximum distance in blocks between surgeon and patient to perform surgery
max-surgery-distance: 5.0
@@ -9,95 +23,44 @@ max-surgery-distance: 5.0
patient-leave-distance-multiplier: 2.0
# ============================================
-# DIAGNOSES
-# ============================================
-
-# List of possible diagnoses/injuries
-# These are randomly assigned when the ultrasound is used
-diagnoses:
- - "Torn Tongue Muscle"
- - "Paper Cuts"
- - "Grumbleteeth"
- - "Chicken Feet"
- - "Broken Heart"
- - "Brain Eating Amoeba"
- - "Ecto-Bones"
- - "Moldy Guts"
- - "Fatty Liver"
- - "Arcane Infection"
- - "Lupus"
- - "Broken Arm"
- - "Broken Leg"
- - "Bird Flu"
- - "Turtle Flu"
- - "Monkey Flu"
- - "Lung Tumor"
- - "Brain Tumor"
- - "Liver Infection"
- - "Kidney Failure"
- - "Appendicitis"
- - "Nose Job"
- - "Herniated Disc"
- - "Heart Attack"
- - "Swallowed Ingot"
- - "Broken Everything"
- - "Serious Head Injury"
- - "Serious Trauma"
- - "Massive Trauma"
-
-# Diagnoses that are flu types (require temperature at 98.6°F to use surgical glove)
-flu-diagnoses:
- - "Bird Flu"
- - "Turtle Flu"
- - "Monkey Flu"
-
-# Required incisions for each diagnosis to unlock the surgical glove and "Fix" the patient
-required-incisions:
- "Nose Job": 1
- "Lung Tumor": 1
- "Heart Attack": 2
- "Brain Tumor": 5
- "Liver Infection": 2
- "Kidney Failure": 2
- "Appendicitis": 3
- "Swallowed Ingot": 2
- "Herniated Disc": 3
- "Serious Head Injury": 4
- "Serious Trauma": 2
- "Massive Trauma": 3
- "Torn Tongue Muscle": 1
- "Paper Cuts": 2
- "Grumbleteeth": 1
- "Chicken Feet": 1
- "Broken Heart": 1
- "Brain Eating Amoeba": 5
- "Ecto-Bones": 1
- "Moldy Guts": 2
- "Fatty Liver": 3
- "Arcane Infection": 3
- "Lupus": 3
- "Broken Arm": 2
- "Broken Leg": 2
- "Broken Everything": 4
-
-# ============================================
-# BONE COUNTS
-# ============================================
-
-# Number of broken and shattered bones for bone-related diagnoses
-bone-counts:
- "Broken Arm":
- broken: 2
- shattered: 0
- "Broken Leg":
- broken: 2
- shattered: 1
- "Ecto-Bones":
- broken: 1
- shattered: 2
- "Broken Everything":
- broken: 3
- shattered: 2
+# PROCEDURES
+# ============================================
+
+# How each healing ailment is operated on, keyed by RPCharacters trait ID.
+# The stethoscope examination reveals the procedure. Once required-incisions
+# are made, the bones are exposed; after every bone is set, the linen dressing
+# treats the ailment.
+# Complications:
+# haemorrhage - the wound keeps reopening and bleeding
+# shock - the patient may collapse under chloroform
+# sepsis - the patient is feverish and the fever keeps climbing
+procedures:
+ broken_arm:
+ name: "Fractured Arm"
+ required-incisions: 2
+ broken-bones: 2
+ shattered-bones: 0
+ complications: []
+ broken_leg:
+ name: "Compound Fracture of the Leg"
+ required-incisions: 2
+ broken-bones: 2
+ shattered-bones: 1
+ complications: [haemorrhage]
+ half_blind:
+ name: "Injured Eye"
+ required-incisions: 1
+ broken-bones: 0
+ shattered-bones: 0
+ complications: [shock, sepsis]
+
+# Used for healing ailments with no procedure above
+default-procedure:
+ name: "Injury"
+ required-incisions: 2
+ broken-bones: 0
+ shattered-bones: 0
+ complications: []
# ============================================
# SKILL FAIL CHANCES
@@ -120,9 +83,7 @@ temperature:
red-temp-threshold: 106.0 # Red temp danger zone
success-threshold: 100.0 # Must be <= this to succeed
rise-rate: 1.8 # Temp increase per move
- antibiotics-change: 5.4 # ±F from antibiotics
- flu-temp-min: 99.0 # Min initial temp for flu diagnoses
- flu-temp-max: 104.0 # Max initial temp for flu diagnoses
+ tincture-change: 5.4 # ±F from the willow-bark tincture
# ============================================
# PULSE SETTINGS
@@ -137,13 +98,13 @@ pulse:
# ============================================
death-timers:
- defibrillator-countdown: 2 # Moves until death after heart stops
+ collapse-countdown: 2 # Moves until death after the patient collapses
weak-pulse-turns: 2 # Consecutive turns with Extremely Weak pulse
red-temp-turns: 2 # Consecutive turns with red temperature
- anesthetic-reuse-cooldown: 4 # Moves before anesthetic can be reused
+ anesthetic-reuse-cooldown: 4 # Moves before chloroform can be given again
# ============================================
-# ANESTHETIC WEAR-OFF
+# CHLOROFORM WEAR-OFF
# ============================================
anesthetic:
@@ -151,46 +112,27 @@ anesthetic:
coming-to-moves: 2 # Moves of "Coming to" before patient is fully awake
# ============================================
-# BONE MECHANICS
-# ============================================
-
-bones:
- # Chance to randomly reveal a bone with each scalpel use for non-bone diagnoses
- random-reveal-chance: 0.25
-
-# ============================================
-# DIAGNOSIS-SPECIFIC MECHANICS
+# COMPLICATIONS
# ============================================
-diagnosis-mechanics:
- moldy-guts:
- bleeding-interval-min: 3 # Forces bleeding every 3-4 moves
+complications:
+ haemorrhage:
+ bleeding-interval-min: 3 # Forces bleeding every 3-4 moves since the last sponge
bleeding-interval-max: 4
-
- fatty-liver:
- heart-stop-chance: 0.20 # 20% chance heart stops when unconscious
-
- broken-heart:
- heart-stop-chance: 0.35 # 35% chance heart stops when unconscious
-
- arcane-infection:
- chaos-chance: 0.25 # 25% chance for chaos each move
- temp-spike-max: 4.0 # Random temp spike up to +4°F
- temp-drop-max: 2.0 # Random temp drop up to -2°F
-
- lupus:
- howl-chance: 0.15 # 15% chance for howl effect
-
- paper-cuts:
- scalpel-uses-required: 2 # Scalpel uses to examine wounds
+ shock:
+ collapse-chance: 0.20 # Chance per move to collapse while unconscious
+ sepsis:
+ fever-min: 100.0 # Fever found by the examination
+ fever-max: 104.0
# ============================================
# COMMANDS
# ============================================
# Commands to execute on surgery completion
-# Use %surgeon% for the surgeon's name and %player% for the patient's name
+# Use %surgeon% for the surgeon's name, %player% for the patient's name,
+# and %ailment% for the ailment
# These are executed by the console
commands:
- surgery-success: "sudo %surgeon% me Successfully treated %player%!"
- surgery-failure: "sudo %surgeon% me Failed surgery on %player%!"
+ surgery-success: "sudo %surgeon% me Successfully operated on %player%'s %ailment%!"
+ surgery-failure: "sudo %surgeon% me Failed to mend %player%'s %ailment%!"
diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml
index e0de532..1a8eec6 100644
--- a/src/main/resources/messages.yml
+++ b/src/main/resources/messages.yml
@@ -2,7 +2,7 @@
# This file contains all text displayed to players
# Command Messages
-command-usage: "&cUsage: /surgery "
+command-usage: "&cUsage: /surgery | accept | deny"
command-player-not-found: "&cPlayer not found or not online!"
command-too-far: "&cThe patient must be within %distance% blocks of you!"
command-console: "&cThis command can only be used by players."
@@ -10,62 +10,77 @@ command-self-surgery: "&cYou cannot perform surgery on yourself!"
command-already-in-surgery: "&cYou are already performing surgery! Close the current menu first."
command-patient-in-surgery: "&cThat player is already undergoing surgery!"
command-patient-is-operating: "&cThat player is busy performing surgery themselves!"
+command-no-permission: "&cOnly a trained &6Physician &ccan perform surgery."
+command-no-ailment: "&e%patient% has no ailment that surgery can treat."
+
+# Consent
+request-sent: "&7You offered to operate on &f%patient%&7's &f%ailment%&7. Waiting for them to accept..."
+request-received: "&6Dr. %surgeon% &eoffers to operate on your &f%ailment% &e(&f%remaining%&e left to heal). &7If the surgery fails, it will take &c%extra% &7longer to heal. This offer expires in %seconds%s."
+request-accept-button: "&a&l[Accept]"
+request-deny-button: "&c&l[Deny]"
+request-none: "&cYou have no surgery offer to answer."
+request-surgeon-gone: "&cThe physician is no longer available."
+request-cannot-start: "&cDr. %surgeon% cannot begin the surgery right now."
+request-ailment-gone: "&eThat ailment has already healed."
+request-accepted-patient: "&aYou lie down on the table. Dr. %surgeon% begins the operation."
+request-accepted-surgeon: "&a%patient% has agreed to the operation."
+request-denied-patient: "&7You declined the operation."
+request-denied-surgeon: "&c%patient% declined the operation."
# Item Errors
-item-not-in-inventory: "&cYou don't have this item in your inventory!"
+item-not-in-inventory: "&cYou don't have this instrument in your inventory!"
# Success/Failure Messages
surgery-successful: "&6Surgery Successful!"
-surgery-successful-subtitle: "&aYou have successfully treated the patient!"
+surgery-successful-subtitle: "&aYou have mended %patient%'s %ailment%!"
+patient-surgery-successful: "&aDr. %surgeon% has mended your %ailment%."
surgery-failed: "&cSurgery Failed!"
+surgery-failed-penalty: "&7The patient's recovery will take &c%extra% &7longer."
+patient-surgery-failed: "&cThe surgery failed. Your %ailment% will take %extra% longer to heal (%remaining% left)."
+surgery-cancelled: "&7You set down your instruments before operating. No harm done."
+ailment-already-healed: "&eThe patient's %ailment% had already healed by the time you finished."
# Failure Reasons
-failure-not-resuscitated: "&cThe patient was not resuscitated in time."
-failure-stabbed-awake: "&cYou just stabbed someone who was fully awake!"
+failure-not-resuscitated: "&cThe patient could not be revived in time."
+failure-stabbed-awake: "&cYou cut into a patient who was wide awake!"
failure-bled-out: "&cThe patient has bled out!"
failure-infection: "&cThe patient has succumbed to infection."
-failure-gave-up: "&cYou couldn't save them!"
-failure-anesthetic-misuse: "&cYou put the patient to sleep. Permanently."
-failure-weak-pulse: "&cThe patient's heart has stopped beating."
-failure-no-diagnosis: "&cYou cut into the patient without knowing what was wrong!"
-failure-high-fever: "&cThe patient died from severe hyperthermia!"
+failure-gave-up: "&cYou abandoned the patient mid-operation!"
+failure-anesthetic-misuse: "&cToo much chloroform. The patient will not wake."
+failure-weak-pulse: "&cThe patient's heart has given out."
+failure-high-fever: "&cThe patient burned up with fever!"
failure-patient-left: "&cThe patient is no longer on the operating table!"
# Progress Messages
bleeding-warning: "&cIt's becoming hard to see..."
-wounds-examined: "&aYou have examined the wounds"
-temperature-reduced: "&aTemperature reduced by 3°C!"
-operation-clean: "&aOperation site is clean. Temperature will not rise."
+examined: "&aExamination complete: &f%procedure%&a. Make %incisions% incision(s) to reach it."
+temperature-reduced: "&aThe tincture brings the fever down."
+operation-clean: "&aCarbolic acid has cleaned the site. The fever will not rise."
vision-cleared: "&aVision cleared! Skill fail chance reduced for next move."
-condition-treated-incomplete: "&eCondition treated, but surgery not yet complete."
+condition-treated-incomplete: "&eThe ailment is dressed, but the surgery is not yet complete."
check-remaining: "&eCheck: bones, incisions, bleeding, temperature, status"
-surgical-glove-ready: "&aSurgical Glove is ready - you can fix the problem now!"
+dressing-ready: "&aThe injury is set - apply the linen dressing!"
# Discovery Messages
-discovered-broken-bone: "&eYou discovered a broken bone!"
-discovered-shattered-bone: "&eYou discovered a shattered bone!"
+discovered-broken-bone: "&eThe incision reveals a broken bone!"
+discovered-shattered-bone: "&eThe incision reveals a shattered bone!"
# Tool Availability Messages
-defibrillator-available: "&cDefibrillator is now available!"
-pins-available: "&eSurgical Pins are now available!"
-splint-available: "&eSurgical Splint is now available!"
-clamp-available: "&cSurgical Clamp is now available!"
+smelling-salts-available: "&cThe patient has collapsed - reach for the smelling salts!"
+silver-wire-available: "&eSilver wire is ready to bind the shattered bone!"
+splint-available: "&eA splint is ready to set the broken bone!"
+artery-forceps-available: "&cArtery forceps are ready to stop the bleeding!"
# Status Change Messages
pulse-weakening: "&cBlood loss is weakening the patient's pulse!"
-protection-lost: "&cOperation site contaminated! Temperature protection lost."
-patient-coming-to: "&eThe anesthetic is wearing off - the patient is coming to!"
-patient-woke-up: "&cThe patient is fully awake! Re-administer anesthetic before cutting!"
+protection-lost: "&cOperation site contaminated! The carbolic acid no longer protects it."
+patient-coming-to: "&eThe chloroform is wearing off - the patient is coming to!"
+patient-woke-up: "&cThe patient is fully awake! Give more chloroform before cutting!"
-# Diagnosis-Specific Messages
-moldy-guts: "&4The patient's guts burst making it impossible to see anything!"
-fatty-liver-heart-stop: "&4The fat build up has caused the patient's heart to stop!"
-broken-heart-stop: "&4The patient's heart breaks!"
-chaos-temp-spike: "&cArcane energy causes temperature spike!"
-chaos-temp-drop: "&bArcane energy causes temperature drop!"
-chaos-heart-stop: "&4Arcane energy stops the heart!"
-chaos-status-change: "&eArcane energy changes patient status!"
-lupus-howl: "&4The patient howls, causing an incision and bleeding!"
+# Complication Messages
+haemorrhage: "&4The wound haemorrhages, blood pours over everything!"
+shock-collapse: "&4The patient goes grey and collapses from shock!"
+sepsis-found: "&cThe wound is septic - the patient is burning with fever!"
# Skill Fail Messages (Sponge - slot 28)
skill-fail-sponge:
@@ -101,158 +116,113 @@ skill-fail-scalpel:
- "The lighting made you misjudge."
- "You sneezed while cutting."
-# Skill Fail Messages (Stitches - slot 30)
-skill-fail-stitches:
- - "You somehow tied yourself up in stitches."
- - "The suture snapped!"
+# Skill Fail Messages (Catgut Suture - slot 30)
+skill-fail-suture:
+ - "You somehow tied yourself up in catgut."
+ - "The catgut snapped!"
- "You tied the knot wrong."
- "The stitches are too loose."
- - "You poked yourself with the needle."
+ - "You pricked your finger on the needle."
- "The thread tangled."
- "You made uneven stitches."
- - "The needle bent."
+ - "The curved needle bent."
- "You lost count of the stitches."
- - "The suture pulled through."
- - "You stitched skin to glove."
+ - "The catgut pulled through the skin."
+ - "You stitched the wound to your cuff."
- "The knot came undone."
- - "You used the wrong stitch pattern."
- - "The thread broke mid-stitch."
+ - "The catgut had dried out and cracked."
-# Skill Fail Messages (Antibiotics - slot 31)
-skill-fail-antibiotics:
- - "This is the wrong medication! The bacteria like it."
- - "You gave the wrong dosage!"
- - "The antibiotics are expired!"
- - "You mixed incompatible drugs!"
- - "You gave it via the wrong route!"
- - "The patient is allergic!"
+# Skill Fail Messages (Willow-Bark Tincture - slot 31)
+skill-fail-tincture:
+ - "You poured the tincture down the wrong pipe."
+ - "You gave a whole bottle instead of a spoonful!"
+ - "The tincture had turned sour."
+ - "You grabbed the laudanum by mistake!"
+ - "The patient spat it back out."
- "You dropped the vial!"
- - "The antibiotics crystallized!"
- - "You confused the labels, gave the patient Fentanyl instead!"
- - "The medication was contaminated!"
- - "You gave it too quickly!"
- - "The IV line infiltrated!"
- - "You used the wrong antibiotic!"
- - "The patient vomited it up!"
+ - "The tincture was mostly gin."
+ - "You confused the labels and gave cod liver oil."
+ - "The dose was far too weak."
+ - "The patient choked on it."
-# Skill Fail Messages (Antiseptic - slot 32)
-skill-fail-antiseptic:
- - "You spilled antiseptic on your shoes. They are very clean now."
- - "The antiseptic evaporated!"
- - "You used expired antiseptic!"
- - "You sprayed it in your eyes!"
- - "The bottle was empty!"
- - "You grabbed the wrong solution!"
- - "It spilled on the floor!"
- - "The antiseptic is contaminated!"
- - "You forgot to let it dry!"
- - "You wiped it off too soon!"
- - "You mixed it with the wrong solution!"
- - "The spray nozzle broke!"
- - "You used too much!"
- - "You missed the target area!"
+# Skill Fail Messages (Carbolic Acid - slot 32)
+skill-fail-carbolic-acid:
+ - "You splashed carbolic acid on your boots. They are very clean now."
+ - "The carbolic spray sputtered and died."
+ - "The bottle was nearly empty!"
+ - "The fumes made your eyes stream."
+ - "You grabbed the vinegar instead!"
+ - "It spilled across the floor!"
+ - "You diluted it far too much."
+ - "You wiped it off before it could work."
+ - "The spray pump jammed."
+ - "You burned your fingers on the neat acid."
-# Skill Fail Messages (Surgical Glove/Fix It - slot 33)
-skill-fail-surgical-glove:
- - "You screwed it up! Try again."
- - "Your glove tore!"
- - "You contaminated the glove!"
- - "The glove is inside-out!"
- - "You grabbed the wrong size!"
- - "The glove has a hole!"
- - "You double-gloved wrong!"
- - "The powder caused a reaction!"
- - "You can't feel anything!"
- - "The glove doesn't fit you must acquit!"
- - "You dropped a tool!"
- - "Your hand is sweating profusely!"
- - "The glove got stuck!"
- - "You used a latex glove on an allergic patient!"
+# Skill Fail Messages (Linen Dressing - slot 33)
+skill-fail-dressing:
+ - "You wrapped the dressing far too tight."
+ - "The linen unravelled in your hands."
+ - "You dropped the dressing on the floor."
+ - "The lint was filthy."
+ - "You bandaged the wrong side."
+ - "The dressing slipped straight off."
+ - "You ran out of linen halfway."
+ - "You tied the bandage to the table."
-# Skill Fail Messages (Ultrasound - slot 34)
-skill-fail-ultrasound:
- - "You scanned the nurse with your ultrasound!"
- - "The screen is showing static."
- - "You dropped the ultrasound probe."
- - "The gel bottle exploded everywhere."
- - "You scanned yourself by mistake."
- - "The machine is showing error codes."
- - "You forgot to turn it on."
- - "The image is too blurry to read."
- - "You scanned the wrong area."
- - "The battery died mid-scan."
- - "You interpreted the results backwards."
- - "The probe slipped out of your hands."
- - "You spilled ultrasound gel everywhere."
- - "The machine made a terrible noise."
+# Skill Fail Messages (Stethoscope - slot 34)
+skill-fail-stethoscope:
+ - "You listened through the wrong end of the stethoscope."
+ - "All you can hear is your own heartbeat."
+ - "The nurse coughed at the worst moment."
+ - "You pressed it to the patient's elbow."
+ - "The wooden tube has a crack in it."
+ - "You dropped the stethoscope under the table."
+ - "The patient's snoring drowns everything out."
+ - "You forgot what a healthy chest sounds like."
-# Skill Fail Messages (Lab Kit - slot 37)
-skill-fail-lab-kit:
- - "You contaminated the sample."
- - "You spilled the lab kit everywhere."
- - "The test results are inconclusive."
- - "You mixed up the samples."
- - "The equipment malfunctioned."
- - "You dropped the sample on the floor."
- - "The reagents expired."
- - "You forgot to calibrate the machine."
- - "The sample was too small."
- - "You contaminated the work area."
- - "The centrifuge broke."
- - "You used the wrong test."
- - "The sample coagulated."
- - "You spilled blood on the paperwork."
+# Skill Fail Messages (Clinical Thermometer - slot 37)
+skill-fail-thermometer:
+ - "The thermometer slipped and shattered."
+ - "You read it upside down."
+ - "The mercury refuses to move."
+ - "You took your own temperature by mistake."
+ - "The patient bit through the glass!"
+ - "You forgot to shake it down first."
+ - "The markings have worn away."
+ - "You left it in the tea."
-# Skill Fail Messages (Anesthetic - slot 38)
-skill-fail-anesthetic:
- - "You end up inhaling all the anesthetic yourself. You feel woozy."
- - "You gave too much!"
- - "The patient is under-anesthetized!"
- - "You forgot to check allergies!"
- - "The gas mask leaked!"
- - "You mixed the wrong gases!"
- - "The ventilator failed!"
- - "Anesthesia awareness!"
- - "The patient isn't breathing!"
- - "You gave it in the wrong vein!"
- - "The oxygen tank is empty!"
- - "You dropped the mask!"
- - "The dosage calculator was wrong!"
- - "The patient woke up mid-surgery!"
+# Skill Fail Messages (Chloroform - slot 38)
+skill-fail-chloroform:
+ - "You breathed in the chloroform yourself. You feel woozy."
+ - "The cloth dried out before it reached the patient."
+ - "You spilled the chloroform on the floor."
+ - "Not nearly enough drops!"
+ - "The patient held their breath."
+ - "The bottle's stopper is stuck fast."
+ - "The patient struggled free of the cloth."
+ - "The nurse fainted from the fumes."
-# Skill Fail Messages (Defibrillator - slot 39)
-skill-fail-defibrillator:
- - "You electrocuted yourself!"
- - "The pads weren't attached!"
- - "You shocked at the wrong time!"
- - "The battery is dead!"
- - "You forgot to yell 'Clear'!"
- - "The machine malfunctioned!"
- - "You used too much power!"
- - "The gel dried out!"
- - "You placed the pads backwards!"
- - "The defib didn't charge!"
- - "You shocked yourself instead!"
- - "The cables came loose!"
- - "You delivered it during a pulse!"
- - "The pads slipped off!"
+# Skill Fail Messages (Smelling Salts - slot 39)
+skill-fail-smelling-salts:
+ - "You took a whiff yourself and reeled back!"
+ - "The salts have lost their pungency."
+ - "You couldn't unscrew the cap."
+ - "You held it under the patient's chin, not their nose."
+ - "The vial slipped from your fingers."
+ - "The patient barely stirs."
+ - "You waved it far too far away."
+ - "The crystals spilled everywhere."
-# Skill Fail Messages (Pins - slot 40)
-skill-fail-pins:
- - "You jabbed the pin through the artery!"
- - "The pin bent!"
- - "You hammered too hard!"
- - "The pin went in crooked!"
- - "You missed the bone!"
- - "The pin is too short!"
- - "You used the wrong size!"
- - "The pin broke!"
- - "You hit your thumb!"
- - "The alignment is wrong!"
- - "You drove it too deep!"
- - "The pin won't grip!"
- - "You fractured the bone more!"
- - "The pin fell out!"
+# Skill Fail Messages (Silver Wire - slot 40)
+skill-fail-silver-wire:
+ - "The wire snapped as you twisted it!"
+ - "You drilled straight through the bone."
+ - "The wire slipped off the fragment."
+ - "You wired the wrong pieces together."
+ - "The wire is far too short."
+ - "You pricked an artery with the wire!"
+ - "The wire kinked and won't bind."
+ - "You twisted it so tight the bone cracked."
# Skill Fail Messages (Splint - slot 41)
skill-fail-splint:
@@ -271,39 +241,24 @@ skill-fail-splint:
- "It's not immobilizing properly!"
- "You dropped the splint!"
-# Skill Fail Messages (Clamp - slot 42)
-skill-fail-clamp:
- - "The clamp fell out of your hand, oh well."
+# Skill Fail Messages (Artery Forceps - slot 42)
+skill-fail-artery-forceps:
+ - "The forceps fell out of your hand, oh well."
- "You clamped the wrong vessel!"
- - "The clamp slipped off!"
+ - "The forceps slipped off!"
- "You crushed the tissue!"
- - "The clamp is too tight!"
+ - "The ratchet won't catch!"
- "You clamped a nerve!"
- - "The clamp broke!"
- - "You dropped it in the wound!"
- - "It won't stay closed!"
- - "You applied it backwards!"
- - "The spring is too weak!"
- - "You clamped healthy tissue!"
- - "The clamp is contaminated!"
+ - "You dropped them in the wound!"
- "You lost your grip!"
-# Skill Fail Messages (Transfusion - slot 43)
+# Skill Fail Messages (Transfusion Syringe - slot 43)
skill-fail-transfusion:
- "You spilled blood everywhere!"
- - "Wrong blood type!"
- - "The blood bag leaked!"
- - "You mixed up the blood units!"
- - "The blood was expired!"
- - "You dropped the blood bag!"
- - "The IV line kinked!"
- - "You started the transfusion too fast!"
- - "The blood wasn't warmed!"
- - "Transfusion reaction!"
- - "You punctured the bag!"
- - "The blood separated!"
- - "You connected the wrong tubing!"
- - "The filter clogged!"
-
-# Skill Fail Messages (Pins - Ecto-Bones special)
-skill-fail-pins-ecto-bones: "The pin phased through the bone and jabbed an artery, shattering another bone in the process!"
\ No newline at end of file
+ - "The blood clotted in the syringe!"
+ - "Air got into the syringe!"
+ - "The donor fainted mid-transfusion!"
+ - "You pushed the plunger far too fast!"
+ - "The brass syringe jammed."
+ - "The needle came out of the vein."
+ - "You drew blood out instead of putting it in!"
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 43dddb1..3024637 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -3,15 +3,9 @@ version: ${project.version}
name: surgery
author: Justin
api-version: '1.21.10'
-depend: [TLibs]
+depend: [TLibs, RPCharacters]
commands:
surgery:
- description: Opens the surgery menu
- usage: /surgery
- permission: surgery.use
-
-permissions:
- surgery.use:
- description: Allows performing surgery on another player
- default: true
\ No newline at end of file
+ description: Offer surgery to a patient, or accept or deny an offer
+ usage: /surgery | accept | deny
diff --git a/src/main/resources/surgeryItemsConfig.yml b/src/main/resources/surgeryItemsConfig.yml
index f87ab30..b08d16f 100644
--- a/src/main/resources/surgeryItemsConfig.yml
+++ b/src/main/resources/surgeryItemsConfig.yml
@@ -1,18 +1,20 @@
# Surgery Items Configuration
-# Configure the TLibs item paths for all surgery tools
+# TLibs item paths for the Victorian surgical instruments.
+# The MMOItems IDs predate the Victorian redesign; keeping them means tools
+# players already crafted keep working.
items:
sponge: "m.surgery.sponge"
scalpel: "m.surgery.scalpel"
- stitches: "m.surgery.stitches"
- antibiotics: "m.surgery.antibiotics"
- antiseptic: "m.surgery.antiseptic"
- surgical-glove: "m.surgery.surgical_glove"
- ultrasound: "m.surgery.ultrasound"
- lab-kit: "m.surgery.lab_kit"
- anesthetic: "m.surgery.anesthetic"
- defibrillator: "m.surgery.defibrillator"
- pins: "m.surgery.pins"
+ suture: "m.surgery.stitches"
+ tincture: "m.surgery.antibiotics"
+ carbolic-acid: "m.surgery.antiseptic"
+ dressing: "m.surgery.surgical_glove"
+ stethoscope: "m.surgery.ultrasound"
+ thermometer: "m.surgery.lab_kit"
+ chloroform: "m.surgery.anesthetic"
+ smelling-salts: "m.surgery.defibrillator"
+ silver-wire: "m.surgery.pins"
splint: "m.surgery.splint"
- clamp: "m.surgery.clamp"
+ artery-forceps: "m.surgery.clamp"
transfusion: "m.surgery.transfusion"
diff --git a/src/test/java/net/tfminecraft/surgery/procedures/DurationsTest.java b/src/test/java/net/tfminecraft/surgery/procedures/DurationsTest.java
new file mode 100644
index 0000000..d4a4c53
--- /dev/null
+++ b/src/test/java/net/tfminecraft/surgery/procedures/DurationsTest.java
@@ -0,0 +1,35 @@
+package net.tfminecraft.surgery.procedures;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class DurationsTest {
+
+ private static final long HOUR = 3_600_000L;
+
+ @Test
+ void parsesSingleAndCombinedUnits() {
+ assertEquals(12 * HOUR, Durations.parseMs("12h"));
+ assertEquals(90 * 60_000L, Durations.parseMs("90m"));
+ assertEquals(30 * HOUR, Durations.parseMs("1d 6h"));
+ assertEquals(30 * HOUR, Durations.parseMs("1D6H"));
+ assertEquals(0L, Durations.parseMs("0h"));
+ }
+
+ @Test
+ void rejectsInvalidValues() {
+ assertEquals(-1L, Durations.parseMs(null));
+ assertEquals(-1L, Durations.parseMs(""));
+ assertEquals(-1L, Durations.parseMs("12"));
+ assertEquals(-1L, Durations.parseMs("twelve hours"));
+ assertEquals(-1L, Durations.parseMs("12h soon"));
+ }
+
+ @Test
+ void formatsHoursRoundedUp() {
+ assertEquals("12h", Durations.formatHours(12 * HOUR));
+ assertEquals("49h", Durations.formatHours(48 * HOUR + 1));
+ assertEquals("45m", Durations.formatHours(45 * 60_000L));
+ }
+}
diff --git a/src/test/java/net/tfminecraft/surgery/procedures/ProcedureRegistryTest.java b/src/test/java/net/tfminecraft/surgery/procedures/ProcedureRegistryTest.java
new file mode 100644
index 0000000..4aa1bcb
--- /dev/null
+++ b/src/test/java/net/tfminecraft/surgery/procedures/ProcedureRegistryTest.java
@@ -0,0 +1,85 @@
+package net.tfminecraft.surgery.procedures;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.junit.jupiter.api.Test;
+
+class ProcedureRegistryTest {
+
+ private static ProcedureRegistry load(String yaml, List warnings) throws InvalidConfigurationException {
+ YamlConfiguration config = new YamlConfiguration();
+ config.loadFromString(yaml);
+ ProcedureRegistry registry = new ProcedureRegistry();
+ registry.load(config, warnings::add);
+ return registry;
+ }
+
+ @Test
+ void loadsProceduresByTraitIdIgnoringCase() throws InvalidConfigurationException {
+ ProcedureRegistry registry = load("""
+ procedures:
+ Broken_Leg:
+ name: "Compound Fracture"
+ required-incisions: 2
+ broken-bones: 2
+ shattered-bones: 1
+ complications: [haemorrhage, SEPSIS]
+ """, new ArrayList<>());
+
+ Procedure leg = registry.get("broken_leg");
+ assertEquals("Compound Fracture", leg.name());
+ assertEquals(2, leg.requiredIncisions());
+ assertEquals(1, leg.shatteredBones());
+ assertTrue(leg.hasBones());
+ assertTrue(leg.has(Complication.HAEMORRHAGE));
+ assertTrue(leg.has(Complication.SEPSIS));
+ assertFalse(leg.has(Complication.SHOCK));
+ }
+
+ @Test
+ void unknownAilmentsUseTheDefaultProcedure() throws InvalidConfigurationException {
+ ProcedureRegistry registry = load("""
+ default-procedure:
+ name: "Wound"
+ required-incisions: 3
+ """, new ArrayList<>());
+
+ Procedure unknown = registry.get("sprained_wrist");
+ assertEquals("Wound", unknown.name());
+ assertEquals(3, unknown.requiredIncisions());
+ assertFalse(unknown.hasBones());
+ }
+
+ @Test
+ void procedureFieldsFallBackToTheDefault() throws InvalidConfigurationException {
+ ProcedureRegistry registry = load("""
+ default-procedure:
+ required-incisions: 4
+ procedures:
+ half_blind:
+ name: "Injured Eye"
+ """, new ArrayList<>());
+
+ assertEquals(4, registry.get("half_blind").requiredIncisions());
+ }
+
+ @Test
+ void warnsAboutUnknownComplications() throws InvalidConfigurationException {
+ List warnings = new ArrayList<>();
+ ProcedureRegistry registry = load("""
+ procedures:
+ broken_arm:
+ complications: [lupus, shock]
+ """, warnings);
+
+ assertEquals(1, warnings.size());
+ assertTrue(registry.get("broken_arm").has(Complication.SHOCK));
+ }
+}
From 8174d1bba481421f8c50e3cffacd11c2e135892a Mon Sep 17 00:00:00 2001
From: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
Date: Fri, 25 Sep 2026 13:23:51 +0000
Subject: [PATCH 2/2] Keep a pending surgery offer, and announce success only
after the cure.
A second physician could replace an offer the patient was about to accept, and the success command could run when the injury was already gone.
Co-authored-by: Cursor
---
.../surgery/commands/SurgeryCommand.java | 8 +++++++-
.../managers/SurgeryCompletionHandler.java | 13 ++++++++-----
.../surgery/managers/SurgeryRequestManager.java | 15 +++++++++++++++
src/main/resources/messages.yml | 1 +
4 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java b/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java
index 566247b..21cbe7d 100644
--- a/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java
+++ b/src/main/java/net/tfminecraft/surgery/commands/SurgeryCommand.java
@@ -81,9 +81,15 @@ private void offer(Player surgeon, String patientName) {
return;
}
+ long now = System.currentTimeMillis();
+ if (requests.pending(patient.getUniqueId(), now) != null) {
+ surgeon.sendMessage(uiUpdater.getMessage("request-already-pending").replace("%patient%", patient.getName()));
+ return;
+ }
+
int timeoutSeconds = Math.max(10, plugin.getConfig().getInt("request-timeout-seconds", 60));
requests.offer(patient.getUniqueId(), surgeon.getUniqueId(), ailment.traitId(),
- System.currentTimeMillis() + timeoutSeconds * 1000L);
+ now + timeoutSeconds * 1000L);
String ailmentName = ChatColor.stripColor(ailment.displayName());
String penalty = plugin.getConfig().getString("failure-healing-penalty", "12h");
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
index e8a615e..81a40fb 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryCompletionHandler.java
@@ -69,16 +69,19 @@ public void handleSuccess(Player surgeon) {
String ailment = stateManager.getAilmentName(surgeonId);
Player patient = patientOf(surgeonId);
- executeCompletionCommand(surgeon, true);
- stateManager.cleanup(surgeonId);
- closeMenuNextTick(surgeon);
-
- // The ailment may have healed on its own while the operation was running
+ // The ailment may have healed on its own while the operation was running.
+ // Do not announce a success the cure did not actually perform.
if (patient == null || !Ailments.cure(patient, traitId)) {
+ stateManager.cleanup(surgeonId);
+ closeMenuNextTick(surgeon);
surgeon.sendMessage(uiUpdater.getMessage("ailment-already-healed").replace("%ailment%", ailment));
return;
}
+ executeCompletionCommand(surgeon, true);
+ stateManager.cleanup(surgeonId);
+ closeMenuNextTick(surgeon);
+
surgeon.playSound(surgeon.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f);
surgeon.sendMessage(uiUpdater.getMessage("surgery-successful"));
surgeon.sendMessage(uiUpdater.getMessage("surgery-successful-subtitle")
diff --git a/src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java b/src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java
index 6e41626..4b2aa07 100644
--- a/src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java
+++ b/src/main/java/net/tfminecraft/surgery/managers/SurgeryRequestManager.java
@@ -19,6 +19,21 @@ public void offer(UUID patientId, UUID surgeonId, String traitId, long expiresAt
requests.put(patientId, new Request(surgeonId, traitId, expiresAt));
}
+ // ==============================================
+ // The patient's current offer, or null when there is none or it has expired
+ // ==============================================
+ public Request pending(UUID patientId, long now) {
+ Request request = requests.get(patientId);
+ if (request == null) {
+ return null;
+ }
+ if (request.expiresAt() < now) {
+ requests.remove(patientId);
+ return null;
+ }
+ return request;
+ }
+
// ==============================================
// Removes and returns the patient's request if it has not expired
// ==============================================
diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml
index 1a8eec6..5c63e8b 100644
--- a/src/main/resources/messages.yml
+++ b/src/main/resources/messages.yml
@@ -15,6 +15,7 @@ command-no-ailment: "&e%patient% has no ailment that surgery can treat."
# Consent
request-sent: "&7You offered to operate on &f%patient%&7's &f%ailment%&7. Waiting for them to accept..."
+request-already-pending: "&c%patient% already has a surgery offer waiting. They need to accept or deny it first."
request-received: "&6Dr. %surgeon% &eoffers to operate on your &f%ailment% &e(&f%remaining%&e left to heal). &7If the surgery fails, it will take &c%extra% &7longer to heal. This offer expires in %seconds%s."
request-accept-button: "&a&l[Accept]"
request-deny-button: "&c&l[Deny]"