diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java index 12792495..5b49abe9 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java @@ -297,9 +297,6 @@ public double getIncome(Cashflow cashflow) { amount = getWarReparationsReceived(); break; case WAR_REPARATIONS_PAYMENT: - if (!guild.isBase()) { - return 0; - } amount = -getWarReparationsPayment(); break; // No isBase() guard: the guild whose tables won declares it, and the capital picks its @@ -754,11 +751,8 @@ public double getTributeRecieved() { } public double getWarReparationsPayment() { - if (!guild.isBase()) { - return 0.0; - } Faction f = guild.getFaction(); - double base = getReparationsTaxableIncome(); + double base = getTradeGrossIncome(); double paid = 0.0; for (WarReparationsObligation obligation : WarReparationsService.activeObligations(f)) { paid += base * (obligation.getIncomePercent() / 100.0); @@ -779,16 +773,25 @@ public double getWarReparationsReceived() { if (f == null || f.getId().equals(self.getId())) { continue; } - Guild payerGuild = f.getOrCreateMainGuild(); - if (payerGuild == null || payerGuild.getLedger() == null) { + if (f.getGuildHandler() == null) { continue; } - double base = payerGuild.getLedger().getReparationsTaxableIncome(); for (WarReparationsObligation obligation : WarReparationsService.activeObligations(f)) { if (!self.getId().equalsIgnoreCase(obligation.getPayeeFactionId())) { continue; } - total += base * (obligation.getIncomePercent() / 100.0); + for (Guild payerGuild : f.getGuildHandler().getGuilds()) { + if (payerGuild == null) { + continue; + } + Ledger payerLedger = payerGuild.getLedger(); + if (payerLedger == null || payerLedger.skipsMoneyMovement()) { + continue; + } + TradeBreakdown trade = payerGuild.getTradeBreakdown(); + double grossTradeIncome = trade == null ? 0.0 : trade.getIncome(); + total += grossTradeIncome * (obligation.getIncomePercent() / 100.0); + } } } return total; @@ -798,6 +801,11 @@ public double getWarReparationsReceived() { return getInternalTaxableIncome(); } + private double getTradeGrossIncome() { + TradeBreakdown trade = guild.getTradeBreakdown(); + return trade == null ? 0.0 : Math.max(0.0, trade.getIncome()); + } + /** * Positive gross-counted income excluding cross-faction transfers (tribute, * war reparations, vassal guild rollups). Used as the base for tribute and @@ -1104,12 +1112,9 @@ private void applySettlementFor(Cashflow cf, DailyGuildTransfers buffer) { } case WAR_REPARATIONS_PAYMENT: { - if (!guild.isBase()) { - return; - } Faction f = guild.getFaction(); if (f == null) return; - double base = getReparationsTaxableIncome(); + double base = getTradeGrossIncome(); for (WarReparationsObligation obligation : WarReparationsService.activeObligations(f)) { if (obligation == null) continue; Faction receiverFaction = FactionManager.getByString(obligation.getPayeeFactionId()); diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/RelationCreator.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/RelationCreator.java index 7198d77f..926076ff 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/RelationCreator.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/RelationCreator.java @@ -175,7 +175,7 @@ public ItemStack createWarReparationsItem(Faction origin, Faction target) { } else { if (paying != null) { lore.add(StringFormatter.formatHex("#c74c3fPaying "+target.getName())); - lore.add(StringFormatter.formatHex("#a89977"+Formatter.formatDouble(paying.getIncomePercent())+"% of main guild income")); + lore.add(StringFormatter.formatHex("#a89977"+Formatter.formatDouble(paying.getIncomePercent())+"% of each guild's gross trade income")); lore.add(StringFormatter.formatHex("#a89977"+paying.getDaysRemaining()+" day(s) remaining")); } if (receiving != null) { @@ -183,12 +183,13 @@ public ItemStack createWarReparationsItem(Faction origin, Faction target) { lore.add(" "); } lore.add(StringFormatter.formatHex("#87d65cReceiving from "+target.getName())); - lore.add(StringFormatter.formatHex("#a89977"+Formatter.formatDouble(receiving.getIncomePercent())+"% of their main guild income")); + lore.add(StringFormatter.formatHex("#a89977"+Formatter.formatDouble(receiving.getIncomePercent())+"% of each payer guild's gross trade income")); lore.add(StringFormatter.formatHex("#a89977"+receiving.getDaysRemaining()+" day(s) remaining")); } } lore.add(" "); - lore.add(StringFormatter.formatHex("#7a7a7aBased on internal taxable income")); + lore.add(StringFormatter.formatHex("#7a7a7aIncludes every guild in each vassal chain")); + lore.add(StringFormatter.formatHex("#7a7a7aCalculated before trade upkeep")); m.setLore(lore); i.setItemMeta(m); return i; diff --git a/src/main/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsService.java b/src/main/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsService.java index 67c2bb80..a5087596 100644 --- a/src/main/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsService.java +++ b/src/main/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsService.java @@ -2,10 +2,13 @@ import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.objects.Faction; +import net.tfminecraft.simplefactions.managers.RelationManager; import net.tfminecraft.simplefactions.war.core.War; public final class WarReparationsService { @@ -32,10 +35,41 @@ public static boolean apply(Faction payer, Faction payee, double percent, int da if (days <= 0 || percent <= 0) { return false; } - payer.addWarReparationsObligation(new WarReparationsObligation(payee.getId(), percent, days)); + for (Faction includedPayer : payerAndVassals(payer)) { + includedPayer.addWarReparationsObligation( + new WarReparationsObligation(payee.getId(), percent, days)); + } return true; } + /** Returns the defeated faction and its full vassal tree, once each. */ + private static List payerAndVassals(Faction root) { + Set visitedIds = new LinkedHashSet<>(); + List result = new ArrayList<>(); + collectPayers(root, visitedIds, result); + return result; + } + + private static void collectPayers(Faction faction, Set visitedIds, List result) { + if (faction == null || faction.getId() == null || !visitedIds.add(faction.getId().toLowerCase())) { + return; + } + result.add(faction); + List subjects; + try { + subjects = RelationManager.getSubjects(faction); + } catch (RuntimeException ignored) { + // A faction being removed during settlement has no subjects to process. + return; + } + if (subjects == null) { + return; + } + for (Faction subject : subjects) { + collectPayers(subject, visitedIds, result); + } + } + public static void tickAfterDailySettlement(Faction payer) { if (payer == null) { return; diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/income/WarReparationsLedgerTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/income/WarReparationsLedgerTest.java index d3396604..47d7262a 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/income/WarReparationsLedgerTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/income/WarReparationsLedgerTest.java @@ -49,6 +49,7 @@ void payerMainGuild_paymentIsNegativePercentOfGross() { FactionManager.factions.add(payee); Guild payerGuild = mockGuild(payer, true, 200.0); + payerGuild.getTradeBreakdown().setUpkeep(125.0); when(payer.getOrCreateMainGuild()).thenReturn(payerGuild); Ledger ledger = new Ledger(payerGuild); @@ -56,32 +57,42 @@ void payerMainGuild_paymentIsNegativePercentOfGross() { } @Test - void payeeMainGuild_incomeIsPositiveFromPayerGross() { + void payeeMainGuild_receivesDirectlyFromAllGuildsInPayerVassalTree() { Faction payer = mockFaction("atk"); + Faction vassal = mockFaction("vassal"); Faction payee = mockFaction("def"); when(payer.getWarReparationsObligations()).thenReturn(List.of( new WarReparationsObligation("def", 25, 10))); + when(vassal.getWarReparationsObligations()).thenReturn(List.of( + new WarReparationsObligation("def", 25, 10))); when(payee.getWarReparationsObligations()).thenReturn(List.of()); FactionManager.factions.add(payer); + FactionManager.factions.add(vassal); FactionManager.factions.add(payee); Guild payerGuild = mockGuild(payer, true, 200.0); + Guild payerBranch = mockGuild(payer, false, 80.0); + Guild vassalMainGuild = mockGuild(vassal, true, 120.0); Guild payeeGuild = mockGuild(payee, true, 0.0); when(payer.getOrCreateMainGuild()).thenReturn(payerGuild); when(payee.getOrCreateMainGuild()).thenReturn(payeeGuild); + when(payer.getGuildHandler().getGuilds()).thenReturn(List.of(payerGuild, payerBranch)); + when(vassal.getGuildHandler().getGuilds()).thenReturn(List.of(vassalMainGuild)); Ledger payeeLedger = new Ledger(payeeGuild); - assertEquals(50.0, payeeLedger.getIncome(Cashflow.WAR_REPARATIONS)); + assertEquals(100.0, payeeLedger.getIncome(Cashflow.WAR_REPARATIONS)); + assertEquals(-20.0, payerBranch.getLedger().getIncome(Cashflow.WAR_REPARATIONS_PAYMENT)); + assertEquals(-30.0, vassalMainGuild.getLedger().getIncome(Cashflow.WAR_REPARATIONS_PAYMENT)); } @Test - void subsidiaryGuild_reparationsAreZero() { + void subsidiaryGuild_paysFromItsOwnGrossTrade() { Faction payer = mockFaction("atk"); when(payer.getWarReparationsObligations()).thenReturn(List.of( new WarReparationsObligation("def", 25, 10))); Guild sub = mockGuild(payer, false, 200.0); Ledger ledger = new Ledger(sub); - assertEquals(0.0, ledger.getIncome(Cashflow.WAR_REPARATIONS_PAYMENT)); + assertEquals(-50.0, ledger.getIncome(Cashflow.WAR_REPARATIONS_PAYMENT)); assertEquals(0.0, ledger.getIncome(Cashflow.WAR_REPARATIONS)); } @@ -102,6 +113,7 @@ void tributeAndReparationsTogether_doNotRecursivelyOverflow() { Guild suzerainGuild = mockGuild(suzerain, true, 0.0); when(tributary.getOrCreateMainGuild()).thenReturn(tributaryGuild); when(suzerain.getOrCreateMainGuild()).thenReturn(suzerainGuild); + when(tributary.getGuildHandler().getGuilds()).thenReturn(List.of(tributaryGuild)); Ledger suzerainLedger = new Ledger(suzerainGuild); Ledger tributaryLedger = new Ledger(tributaryGuild); diff --git a/src/test/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsServiceTest.java b/src/test/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsServiceTest.java index 2b4b46ca..04748969 100644 --- a/src/test/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsServiceTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/war/resolution/WarReparationsServiceTest.java @@ -12,9 +12,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import net.tfminecraft.simplefactions.Cache; import net.tfminecraft.simplefactions.objects.Faction; +import net.tfminecraft.simplefactions.managers.RelationManager; import net.tfminecraft.simplefactions.war.core.War; import net.tfminecraft.simplefactions.war.enums.WarGoalType; import net.tfminecraft.simplefactions.war.enums.WarType; @@ -78,6 +80,36 @@ void applyFromWar_attackerPaysDefenderWithCacheValues() { assertEquals(10, obligations.get(0).getDaysRemaining()); } + @Test + void apply_recursivelyAddsObligationToEveryVassalOnce() { + Faction child = mockFaction("child"); + Faction grandchild = mockFaction("grandchild"); + List childObligations = new ArrayList<>(); + List grandchildObligations = new ArrayList<>(); + when(child.getWarReparationsObligations()).thenReturn(childObligations); + when(grandchild.getWarReparationsObligations()).thenReturn(grandchildObligations); + doAnswer(invocation -> { + childObligations.add(invocation.getArgument(0)); + return null; + }).when(child).addWarReparationsObligation(org.mockito.ArgumentMatchers.any()); + doAnswer(invocation -> { + grandchildObligations.add(invocation.getArgument(0)); + return null; + }).when(grandchild).addWarReparationsObligation(org.mockito.ArgumentMatchers.any()); + + try (MockedStatic relations = org.mockito.Mockito.mockStatic(RelationManager.class)) { + relations.when(() -> RelationManager.getSubjects(payer)).thenReturn(List.of(child)); + relations.when(() -> RelationManager.getSubjects(child)).thenReturn(List.of(grandchild)); + relations.when(() -> RelationManager.getSubjects(grandchild)).thenReturn(List.of(payer)); + assertTrue(WarReparationsService.apply(payer, payee)); + } + + assertEquals(1, obligations.size()); + assertEquals(1, childObligations.size()); + assertEquals(1, grandchildObligations.size()); + assertEquals("def", grandchildObligations.get(0).getPayeeFactionId()); + } + private static Faction mockFaction(String id) { Faction faction = mock(Faction.class); when(faction.getId()).thenReturn(id);