@@ -41,23 +41,28 @@ public static void loadAptitudes(java.nio.file.Path file) throws java.io.IOExcep
4141 }
4242
4343 public static boolean eligible (Faction faction , String playerName ) {
44- if (!faction .isMemberIgnoreCase (playerName ) && !faction .isLeader (playerName )) return false ;
45- return !faction .isLeader (playerName ) || faction .getMembers ().stream ()
46- .allMatch (name -> name .equalsIgnoreCase (playerName ));
44+ return faction .isMemberIgnoreCase (playerName ) || faction .isLeader (playerName );
4745 }
4846
49- /** Only new factions pass through addFaction; loading saves never reassigns offices. */
47+ /** The leader's default holding follows the leadership; deliberate appointments stay. */
48+ private static boolean validHolder (Faction faction , SpecialPositionAssignment holder ) {
49+ return holder != null && eligible (faction , holder .playerName ) && !deadCharacter (holder )
50+ && !(holder .automatic && !faction .isLeader (holder .playerName ));
51+ }
52+
53+ /** New factions and every later vacancy fall to the leader; it waits for their active character. */
5054 public static void initializeFounder (Faction faction ) {
5155 initializeFounder (faction , null );
5256 }
5357
5458 private static void initializeFounder (Faction faction , java .util .Set <Faction > dirty ) {
55- if (faction .getEspionage () == null || org .bukkit .Bukkit .getServer () == null ) return ;
59+ if (faction .getEspionage () == null || faction . getLeader () == null || org .bukkit .Bukkit .getServer () == null ) return ;
5660 var founder = org .bukkit .Bukkit .getPlayerExact (faction .getLeader ());
5761 String characterId = OfficeCharacters .activeCharacterId (founder );
62+ // A dead character would be revoked again on the next check.
63+ if (characterId != null && founder != null && OfficeCharacters .isDead (founder .getUniqueId (), characterId )) characterId = null ;
5864 for (SpecialPosition office : SpecialPosition .values ()) {
5965 if (faction .getEspionage ().holder (office ) != null ) continue ;
60- if (office == SpecialPosition .SPYMASTER && !eligible (faction , faction .getLeader ())) continue ;
6166 if (characterId == null ) {
6267 faction .getEspionage ().pendingFounder (office );
6368 continue ;
@@ -124,7 +129,23 @@ static boolean completeAndSaveAppointment(Faction faction, SpecialPositionAssign
124129
125130 public static int effectiveAptitude (Faction faction , SpecialPositionAssignment holder ) {
126131 if (holder == null || !eligible (faction , holder .playerName )) return 0 ;
127- return faction .isLeader (holder .playerName ) ? (int ) Math .floor (holder .aptitude * EspionageConfig .soloMultiplier ()) : holder .aptitude ;
132+ // The epsilon keeps 1 - 0.1 * 3 from flooring a whole result one point low.
133+ return (int ) Math .floor (holder .aptitude * positionMultiplier (positionsHeld (faction , holder )) + 1e-9 );
134+ }
135+
136+ /** Offices held by the same person; an assignment not saved yet still counts as one. */
137+ public static int positionsHeld (Faction faction , SpecialPositionAssignment holder ) {
138+ if (holder == null || holder .playerName == null || faction .getEspionage () == null ) return 1 ;
139+ int held = 0 ;
140+ for (SpecialPosition office : SpecialPosition .values ()) {
141+ var other = faction .getEspionage ().holder (office );
142+ if (other != null && holder .playerName .equalsIgnoreCase (other .playerName )) held ++;
143+ }
144+ return Math .max (1 , held );
145+ }
146+
147+ public static double positionMultiplier (int held ) {
148+ return Math .max (0 , 1 - EspionageConfig .extraPositionPenalty () * Math .max (0 , held - 1 ));
128149 }
129150
130151 public static boolean isOwn (Player viewer , Faction target ) {
@@ -145,8 +166,7 @@ public static boolean canViewExact(Player viewer, Faction target) {
145166 /** A vacant, ineligible or deceased holder leaves every guild's information unguarded. */
146167 public static boolean hasSpymaster (Faction faction ) {
147168 if (faction == null || faction .getEspionage () == null ) return false ;
148- var holder = faction .getEspionage ().getSpymaster ();
149- return holder != null && eligible (faction , holder .playerName ) && !deadCharacter (holder );
169+ return validHolder (faction , faction .getEspionage ().getSpymaster ());
150170 }
151171
152172 public static SpecialPositionAssignment spymaster (Faction faction ) {
@@ -159,17 +179,24 @@ private static void saveOrMark(Faction faction, java.util.Set<Faction> dirty) {
159179 }
160180
161181 private static SpecialPositionAssignment spymaster (Faction faction , java .util .Set <Faction > dirty ) {
162- if (faction .getEspionage ().hasPendingFounder ()) {
163- initializeFounder (faction , dirty );
164- if (!faction .getEspionage ().hasPendingFounder ()) saveOrMark (faction , dirty );
182+ var state = faction .getEspionage ();
183+ boolean changed = false ;
184+ SpecialPositionAssignment holder = state .getSpymaster ();
185+ if (holder != null && !validHolder (faction , holder )) {
186+ state .removeSpymaster ();
187+ changed = true ;
165188 }
166- SpecialPositionAssignment holder = faction .getEspionage ().getSpymaster ();
167- if (holder != null && (!eligible (faction , holder .playerName ) || deadCharacter (holder ))) {
168- faction .getEspionage ().removeSpymaster ();
169- saveOrMark (faction , dirty );
170- return null ;
189+ // Whatever emptied the office, it falls back to the current leader.
190+ if (state .getSpymaster () == null && !state .isPendingFounder (SpecialPosition .SPYMASTER ) && faction .getLeader () != null ) {
191+ state .pendingFounder (SpecialPosition .SPYMASTER );
192+ changed = true ;
193+ }
194+ if (state .hasPendingFounder ()) {
195+ initializeFounder (faction , dirty );
196+ if (!state .hasPendingFounder ()) changed = true ;
171197 }
172- return holder ;
198+ if (changed ) saveOrMark (faction , dirty );
199+ return state .getSpymaster ();
173200 }
174201
175202 private static boolean deadCharacter (SpecialPositionAssignment holder ) {
@@ -190,16 +217,15 @@ public static void characterDied(Player owner, String characterId, String charac
190217 state .removeSpymaster ();
191218 new Database ().saveFaction (faction );
192219 if (owner != null ) owner .sendMessage ("\u00a7 8\u00a7 oWith the passing of " + characterName
193- + ", the keys to the Spymaster's office return to the faction. The office awaits a successor." );
220+ + ", the keys to the Spymaster's office return to the faction leader until a successor is appointed ." );
194221 }
195222 }
196223
197224 public static java .util .List <net .tfminecraft .simplefactions .government .StabilityModifier > stabilityModifiers (Faction faction , long now ) {
198225 var result = new java .util .ArrayList <>(faction .getEspionage ().unrestModifiers (now ));
199226 for (SpecialPosition office : SpecialPosition .values ()) {
200227 var holder = faction .getEspionage ().holder (office );
201- boolean occupied = holder != null && (office != SpecialPosition .SPYMASTER
202- || eligible (faction , holder .playerName ) && !deadCharacter (holder ));
228+ boolean occupied = office != SpecialPosition .SPYMASTER ? holder != null : validHolder (faction , holder );
203229 double penalty = EspionageConfig .vacancyPenalty (office );
204230 if (!occupied && penalty > 0 ) result .add (new net .tfminecraft .simplefactions .government .StabilityModifier (
205231 "Vacant " + office .label (), -penalty , 0 ));
@@ -216,18 +242,13 @@ public static boolean appoint(Player actor, Faction faction, Player candidate) {
216242 actor .sendMessage ("§cChoose an online member of your own faction." );
217243 return false ;
218244 }
219- if (!eligible (faction , candidate .getName ())) {
220- actor .sendMessage ("§cThe faction leader cannot be Spymaster unless the faction has only one member." );
221- return false ;
222- }
223245 String characterId = OfficeCharacters .activeCharacterId (candidate );
224246 if (characterId == null ) {
225247 actor .sendMessage ("§cThat member needs an active roleplay character." );
226248 return false ;
227249 }
228250 SpecialPositionAssignment current = spymaster (faction );
229- if (current != null && !current .automatic && current .isHolder (candidate .getUniqueId ())
230- && characterId .equals (current .characterId )) {
251+ if (current != null && current .isHolder (candidate .getUniqueId ()) && characterId .equals (current .characterId )) {
231252 actor .sendMessage ("§7That member is already your Spymaster." );
232253 return false ;
233254 }
@@ -259,8 +280,9 @@ public static boolean appoint(Player actor, Faction faction, Player candidate) {
259280 + "§8§o's unseen network are now yours. You have been appointed Spymaster." );
260281 candidate .sendMessage ("§7Your aptitude for this office is §e" + effectiveAptitude (faction , assignment )
261282 + "/100§7. Inspect Special Positions in the faction menu, or use §a/faction espionage§7." );
262- if (faction .isLeader (candidate .getName ()))
263- candidate .sendMessage ("§7Leading a one-person faction retains " + Math .round (EspionageConfig .soloMultiplier () * 100 ) + "% aptitude (base: " + assignment .aptitude + ")." );
283+ int held = positionsHeld (faction , assignment );
284+ if (held > 1 ) candidate .sendMessage ("§7Holding " + held + " offices leaves you " + Math .round (positionMultiplier (held ) * 100 )
285+ + "% of your aptitude in each (base: " + assignment .aptitude + ")." );
264286 if (!actor .getUniqueId ().equals (candidate .getUniqueId ())) actor .sendMessage ("§aSpymaster appointed." );
265287 if (repeat ) actor .sendMessage ("\u00a7 7Appointment: " + cost + "d from the treasury. The change of office brings "
266288 + EspionageConfig .stabilityPenalty () + " points of unrest, fading over " + EspionageConfig .penaltyDays () + " days." );
@@ -278,14 +300,20 @@ public static boolean remove(Player actor, Faction faction) {
278300 actor .sendMessage ("§cOnly your faction leader can remove the Spymaster." );
279301 return false ;
280302 }
303+ var current = spymaster (faction );
304+ if (current == null || current .automatic ) {
305+ actor .sendMessage ("§7No Spymaster has been appointed. The office already rests with you as faction leader." );
306+ return false ;
307+ }
281308 var previous = faction .getEspionage ().snapshotOffices ();
282309 faction .getEspionage ().removeSpymaster ();
283310 if (!new Database ().saveFactionChecked (faction )) {
284311 faction .getEspionage ().restoreOffices (previous );
285312 actor .sendMessage ("\u00a7 cThe office removal could not be saved. The Spymaster remains appointed." );
286313 return false ;
287314 }
288- actor .sendMessage ("§aThe Spymaster office is now vacant." );
315+ actor .sendMessage ("§aThe Spymaster has been dismissed. The office returns to you as faction leader." );
316+ spymaster (faction );
289317 return true ;
290318 }
291319
0 commit comments