Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class EspionageConfig {
private static double stabilityPenalty = 10;
private static double penaltyDays = 7;
private static String reloadPermission = "simplefactions.espionage.reload";
private static double base = 50, soloMultiplier = .25, rollMultiplier = 1.25;
private static double base = 50, extraPositionPenalty = .25, rollMultiplier = 1.25;
private static int center = 6, cap = 16, aptitudeSpread = 20, luckSpread = 75, luckDraws = 3, rosterLimit = 23;
private static final Map<IntelligenceTier, TierSettings> tiers = new java.util.EnumMap<>(IntelligenceTier.class);
private static final Map<String, IntelligenceTier> minimums = new LinkedHashMap<>();
Expand Down Expand Up @@ -68,7 +68,7 @@ public static void load(ConfigurationSection config) {
cap = (int) bounded(config, "espionage.aptitude.attribute-cap", 16, 1, 1000);
center = (int) bounded(config, "espionage.aptitude.attribute-center", 6, 0, cap);
aptitudeSpread = (int) bounded(config, "espionage.aptitude.random-spread", 20, 0, 100);
soloMultiplier = bounded(config, "espionage.aptitude.solo-leader-multiplier", .25, 0, 1);
extraPositionPenalty = bounded(config, "espionage.aptitude.extra-position-penalty", .25, 0, 1);
rollMultiplier = bounded(config, "espionage.checks.aptitude-multiplier", 1.25, .01, 10);
luckSpread = (int) bounded(config, "espionage.checks.luck-spread", 75, 0, 1000);
luckDraws = (int) bounded(config, "espionage.checks.luck-draws", 3, 1, 20);
Expand Down Expand Up @@ -138,7 +138,7 @@ public static boolean allows(IntelligenceTier tier, String metric) {
public static int center() { return center; }
public static int cap() { return cap; }
public static int aptitudeSpread() { return aptitudeSpread; }
public static double soloMultiplier() { return soloMultiplier; }
public static double extraPositionPenalty() { return extraPositionPenalty; }
public static double rollMultiplier() { return rollMultiplier; }
public static int luckSpread() { return luckSpread; }
public static int luckDraws() { return luckDraws; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,28 @@ public static void loadAptitudes(java.nio.file.Path file) throws java.io.IOExcep
}

public static boolean eligible(Faction faction, String playerName) {
if (!faction.isMemberIgnoreCase(playerName) && !faction.isLeader(playerName)) return false;
return !faction.isLeader(playerName) || faction.getMembers().stream()
.allMatch(name -> name.equalsIgnoreCase(playerName));
return faction.isMemberIgnoreCase(playerName) || faction.isLeader(playerName);
}

/** Only new factions pass through addFaction; loading saves never reassigns offices. */
/** The leader's default holding follows the leadership; deliberate appointments stay. */
private static boolean validHolder(Faction faction, SpecialPositionAssignment holder) {
return holder != null && eligible(faction, holder.playerName) && !deadCharacter(holder)
&& !(holder.automatic && !faction.isLeader(holder.playerName));
}

/** New factions and every later vacancy fall to the leader; it waits for their active character. */
public static void initializeFounder(Faction faction) {
initializeFounder(faction, null);
}

private static void initializeFounder(Faction faction, java.util.Set<Faction> dirty) {
if (faction.getEspionage() == null || org.bukkit.Bukkit.getServer() == null) return;
if (faction.getEspionage() == null || faction.getLeader() == null || org.bukkit.Bukkit.getServer() == null) return;
var founder = org.bukkit.Bukkit.getPlayerExact(faction.getLeader());
String characterId = OfficeCharacters.activeCharacterId(founder);
// A dead character would be revoked again on the next check.
if (characterId != null && founder != null && OfficeCharacters.isDead(founder.getUniqueId(), characterId)) characterId = null;
for (SpecialPosition office : SpecialPosition.values()) {
if (faction.getEspionage().holder(office) != null) continue;
if (office == SpecialPosition.SPYMASTER && !eligible(faction, faction.getLeader())) continue;
if (characterId == null) {
faction.getEspionage().pendingFounder(office);
continue;
Expand Down Expand Up @@ -124,7 +129,23 @@ static boolean completeAndSaveAppointment(Faction faction, SpecialPositionAssign

public static int effectiveAptitude(Faction faction, SpecialPositionAssignment holder) {
if (holder == null || !eligible(faction, holder.playerName)) return 0;
return faction.isLeader(holder.playerName) ? (int) Math.floor(holder.aptitude * EspionageConfig.soloMultiplier()) : holder.aptitude;
// The epsilon keeps 1 - 0.1 * 3 from flooring a whole result one point low.
return (int) Math.floor(holder.aptitude * positionMultiplier(positionsHeld(faction, holder)) + 1e-9);
}

/** Offices held by the same person; an assignment not saved yet still counts as one. */
public static int positionsHeld(Faction faction, SpecialPositionAssignment holder) {
if (holder == null || holder.playerName == null || faction.getEspionage() == null) return 1;
int held = 0;
for (SpecialPosition office : SpecialPosition.values()) {
var other = faction.getEspionage().holder(office);
if (other != null && holder.playerName.equalsIgnoreCase(other.playerName)) held++;
}
return Math.max(1, held);
}

public static double positionMultiplier(int held) {
return Math.max(0, 1 - EspionageConfig.extraPositionPenalty() * Math.max(0, held - 1));
}

public static boolean isOwn(Player viewer, Faction target) {
Expand All @@ -145,8 +166,7 @@ public static boolean canViewExact(Player viewer, Faction target) {
/** A vacant, ineligible or deceased holder leaves every guild's information unguarded. */
public static boolean hasSpymaster(Faction faction) {
if (faction == null || faction.getEspionage() == null) return false;
var holder = faction.getEspionage().getSpymaster();
return holder != null && eligible(faction, holder.playerName) && !deadCharacter(holder);
return validHolder(faction, faction.getEspionage().getSpymaster());
}

public static SpecialPositionAssignment spymaster(Faction faction) {
Expand All @@ -159,17 +179,24 @@ private static void saveOrMark(Faction faction, java.util.Set<Faction> dirty) {
}

private static SpecialPositionAssignment spymaster(Faction faction, java.util.Set<Faction> dirty) {
if (faction.getEspionage().hasPendingFounder()) {
initializeFounder(faction, dirty);
if (!faction.getEspionage().hasPendingFounder()) saveOrMark(faction, dirty);
var state = faction.getEspionage();
boolean changed = false;
SpecialPositionAssignment holder = state.getSpymaster();
if (holder != null && !validHolder(faction, holder)) {
state.removeSpymaster();
changed = true;
}
SpecialPositionAssignment holder = faction.getEspionage().getSpymaster();
if (holder != null && (!eligible(faction, holder.playerName) || deadCharacter(holder))) {
faction.getEspionage().removeSpymaster();
saveOrMark(faction, dirty);
return null;
// Whatever emptied the office, it falls back to the current leader.
if (state.getSpymaster() == null && !state.isPendingFounder(SpecialPosition.SPYMASTER) && faction.getLeader() != null) {
state.pendingFounder(SpecialPosition.SPYMASTER);
changed = true;
}
if (state.hasPendingFounder()) {
initializeFounder(faction, dirty);
if (!state.hasPendingFounder()) changed = true;
}
return holder;
if (changed) saveOrMark(faction, dirty);
return state.getSpymaster();
}

private static boolean deadCharacter(SpecialPositionAssignment holder) {
Expand All @@ -190,16 +217,15 @@ public static void characterDied(Player owner, String characterId, String charac
state.removeSpymaster();
new Database().saveFaction(faction);
if (owner != null) owner.sendMessage("\u00a78\u00a7oWith the passing of " + characterName
+ ", the keys to the Spymaster's office return to the faction. The office awaits a successor.");
+ ", the keys to the Spymaster's office return to the faction leader until a successor is appointed.");
}
}

public static java.util.List<net.tfminecraft.simplefactions.government.StabilityModifier> stabilityModifiers(Faction faction, long now) {
var result = new java.util.ArrayList<>(faction.getEspionage().unrestModifiers(now));
for (SpecialPosition office : SpecialPosition.values()) {
var holder = faction.getEspionage().holder(office);
boolean occupied = holder != null && (office != SpecialPosition.SPYMASTER
|| eligible(faction, holder.playerName) && !deadCharacter(holder));
boolean occupied = office != SpecialPosition.SPYMASTER ? holder != null : validHolder(faction, holder);
double penalty = EspionageConfig.vacancyPenalty(office);
if (!occupied && penalty > 0) result.add(new net.tfminecraft.simplefactions.government.StabilityModifier(
"Vacant " + office.label(), -penalty, 0));
Expand All @@ -216,18 +242,13 @@ public static boolean appoint(Player actor, Faction faction, Player candidate) {
actor.sendMessage("§cChoose an online member of your own faction.");
return false;
}
if (!eligible(faction, candidate.getName())) {
actor.sendMessage("§cThe faction leader cannot be Spymaster unless the faction has only one member.");
return false;
}
String characterId = OfficeCharacters.activeCharacterId(candidate);
if (characterId == null) {
actor.sendMessage("§cThat member needs an active roleplay character.");
return false;
}
SpecialPositionAssignment current = spymaster(faction);
if (current != null && !current.automatic && current.isHolder(candidate.getUniqueId())
&& characterId.equals(current.characterId)) {
if (current != null && current.isHolder(candidate.getUniqueId()) && characterId.equals(current.characterId)) {
actor.sendMessage("§7That member is already your Spymaster.");
return false;
}
Expand Down Expand Up @@ -259,8 +280,9 @@ public static boolean appoint(Player actor, Faction faction, Player candidate) {
+ "§8§o's unseen network are now yours. You have been appointed Spymaster.");
candidate.sendMessage("§7Your aptitude for this office is §e" + effectiveAptitude(faction, assignment)
+ "/100§7. Inspect Special Positions in the faction menu, or use §a/faction espionage§7.");
if (faction.isLeader(candidate.getName()))
candidate.sendMessage("§7Leading a one-person faction retains " + Math.round(EspionageConfig.soloMultiplier() * 100) + "% aptitude (base: " + assignment.aptitude + ").");
int held = positionsHeld(faction, assignment);
if (held > 1) candidate.sendMessage("§7Holding " + held + " offices leaves you " + Math.round(positionMultiplier(held) * 100)
+ "% of your aptitude in each (base: " + assignment.aptitude + ").");
if (!actor.getUniqueId().equals(candidate.getUniqueId())) actor.sendMessage("§aSpymaster appointed.");
if (repeat) actor.sendMessage("\u00a77Appointment: " + cost + "d from the treasury. The change of office brings "
+ EspionageConfig.stabilityPenalty() + " points of unrest, fading over " + EspionageConfig.penaltyDays() + " days.");
Expand All @@ -278,14 +300,20 @@ public static boolean remove(Player actor, Faction faction) {
actor.sendMessage("§cOnly your faction leader can remove the Spymaster.");
return false;
}
var current = spymaster(faction);
if (current == null || current.automatic) {
actor.sendMessage("§7No Spymaster has been appointed. The office already rests with you as faction leader.");
return false;
}
var previous = faction.getEspionage().snapshotOffices();
faction.getEspionage().removeSpymaster();
if (!new Database().saveFactionChecked(faction)) {
faction.getEspionage().restoreOffices(previous);
actor.sendMessage("\u00a7cThe office removal could not be saved. The Spymaster remains appointed.");
return false;
}
actor.sendMessage("§aThe Spymaster office is now vacant.");
actor.sendMessage("§aThe Spymaster has been dismissed. The office returns to you as faction leader.");
spymaster(faction);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

/** Migrates the old espionage section once; existing dedicated settings always win. */
public final class SpecialPositionsConfigFile {
// Settings whose rule no longer exists; their comments would mislead staff.
private static final java.util.List<String> RETIRED_KEYS = java.util.List.of("espionage.aptitude.solo-leader-multiplier");
private SpecialPositionsConfigFile() {}

public static void load(JavaPlugin plugin) {
Expand Down Expand Up @@ -54,6 +56,12 @@ else if (legacy.exists()) {
}
boolean changed = !file.exists();
if (changed) settings.options().setHeader(defaults.options().getHeader());
for (String retired : RETIRED_KEYS) {
if (settings.contains(retired)) {
settings.set(retired, null);
changed = true;
}
}
for (var entry : defaults.getValues(true).entrySet()) {
if (!(entry.getValue() instanceof org.bukkit.configuration.ConfigurationSection) && !settings.contains(entry.getKey())) {
settings.set(entry.getKey(), entry.getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,14 @@ public static void spymasterOffice(Player viewer, Faction faction, InventoryMana
Inventory inventory = Bukkit.createInventory(new SFInventoryHolder(faction.getId(), SFGUI.SPYMASTER_VIEW),
27, "\u00a77Spymaster's Office");
SpecialPositionAssignment holder = EspionageService.spymaster(faction);
int held = EspionageService.positionsHeld(faction, holder);
ItemStack head = item(Material.PLAYER_HEAD, "Spymaster: " + (holder == null ? "Vacant" : CharacterNames.display(viewer, holder.playerName)),
"§7Aptitude: §e" + EspionageService.effectiveAptitude(faction, holder) + "/100",
holder != null && faction.isLeader(holder.playerName) ? "§7Solo leader: " + Math.round(EspionageConfig.soloMultiplier() * 100)
+ "% aptitude retained (base " + holder.aptitude + ")." : "§7Faction leaders are ineligible while other members belong.",
holder == null ? "§7Falls to the faction leader once they have an active character."
: held > 1 ? "§7Holds " + held + " offices: " + Math.round(EspionageService.positionMultiplier(held) * 100)
+ "% aptitude in each (base " + holder.aptitude + ")."
: holder.automatic ? "§7Held by the faction leader until a member is appointed."
: "§7Appointed by the faction leader.",
"§7Gathers foreign intelligence and guards your secrets.",
holder == null ? "\u00a7cWithout a Spymaster, all faction and guild information is public."
: "\u00a77An eligible Spymaster protects your faction and guild information.",
Expand All @@ -283,7 +287,7 @@ public static void spymasterOffice(Player viewer, Faction faction, InventoryMana
? "\u00a77The founder's assignment does not use your free appointment."
: "\u00a77Unrest: -" + EspionageConfig.stabilityPenalty() + " points, fading over " + EspionageConfig.penaltyDays() + " days."));
inventory.setItem(15, item(Material.REDSTONE, "Remove Spymaster",
"\u00a7cMakes all faction and guild information public."));
"\u00a77Dismisses the appointee. The office returns to you."));
}
inventory.setItem(26, manager.createBackButton(SFGUI.SPYMASTER_VIEW));
viewer.openInventory(inventory);
Expand Down
6 changes: 4 additions & 2 deletions src/main/resources/special-positions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ espionage:
attribute-center: 6
attribute-cap: 16
random-spread: 20
# A leader can only hold this office in a one-person faction; they retain 25% aptitude.
solo-leader-multiplier: 0.25
# The faction leader holds every vacant office by default. Each office a character holds
# beyond their first costs this fraction of their aptitude in all of them:
# 1 office 100%, 2 offices 75%, 3 offices 50%... Never below 0%. Zero disables this penalty.
extra-position-penalty: 0.25
attribute-weights:
intelligence: 3.0
wisdom: 2.5
Expand Down
Loading
Loading