diff --git a/src/main/java/net/tfminecraft/simplefactions/enums/GuildModifier.java b/src/main/java/net/tfminecraft/simplefactions/enums/GuildModifier.java index bd8dc54e..63ba9796 100644 --- a/src/main/java/net/tfminecraft/simplefactions/enums/GuildModifier.java +++ b/src/main/java/net/tfminecraft/simplefactions/enums/GuildModifier.java @@ -6,6 +6,7 @@ public enum GuildModifier { TRADE_UPKEEP("#d6645aTrade Upkeep", false), PRODUCTION("#f2c94cProduction", true), INFRASTRUCTURE("#86d1b0Infrastructure", true), + INFRASTRUCTURE_UPKEEP("#86d1b0Infrastructure Upkeep", false), TRADE_CARRY("#86d1b0Trade Carry", true), HUB_LIMIT("#c99b70Supply Hub Limit", true), HUB_UPKEEP("#d6645aSupply Hub Upkeep", false), diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/Guild.java b/src/main/java/net/tfminecraft/simplefactions/guild/Guild.java index af6bc349..45478752 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/Guild.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/Guild.java @@ -196,6 +196,7 @@ public Guild(GuildData data, Faction host) { ); } } + BranchLoader.replaceDisallowedBranches(branches, type); int group = 0; while(group < 10) { if(!this.branches.containsKey(group)) { diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreview.java b/src/main/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreview.java index 275eebcf..52f6639e 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreview.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreview.java @@ -59,7 +59,9 @@ public static double estimate( ProvinceManager after = copy(prepared.snapshot); double baseline = income(before, guild, current); double changed = income(after, guild, hypothetical); - double net = (changed - baseline) * (1.0 - taxFraction); + double upkeepChange = Math.max(0.0, hypothetical.getOrDefault(GuildModifier.INFRASTRUCTURE_UPKEEP, 0.0)) + - Math.max(0.0, current.getOrDefault(GuildModifier.INFRASTRUCTURE_UPKEEP, 0.0)); + double net = (changed - baseline) * (1.0 - taxFraction) - upkeepChange; return Math.round(net * 100.0) / 100.0; } diff --git a/src/main/java/net/tfminecraft/simplefactions/guild/income/Cashflow.java b/src/main/java/net/tfminecraft/simplefactions/guild/income/Cashflow.java index 4795e447..d57b03e8 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/income/Cashflow.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/Cashflow.java @@ -50,7 +50,8 @@ public enum Cashflow { VEHICLE_UPKEEP("#a6659fVehicle Upkeep", true, false), MILITARY_UPKEEP("#a6659fMilitary Upkeep", true, false), NODES("#8a7a5cNodes", true, false), - SUPPLY_HUBS("#b5835aSupply hubs", true, false); + SUPPLY_HUBS("#b5835aSupply hubs", true, false), + INFRASTRUCTURE_UPKEEP("#86d1b0Infrastructure", true, false); private final String display; private final boolean affectsInflation; 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 5b49abe9..483a5677 100644 --- a/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java +++ b/src/main/java/net/tfminecraft/simplefactions/guild/income/Ledger.java @@ -14,6 +14,7 @@ import net.tfminecraft.simplefactions.SimpleFactions; import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.GuildModifierOverride; import net.tfminecraft.simplefactions.guild.hub.SupplyHub; import net.tfminecraft.simplefactions.guild.hub.HubTaxBreakdown; import net.tfminecraft.simplefactions.guild.hub.SupplyHubCommands; @@ -33,6 +34,7 @@ import net.tfminecraft.simplefactions.utils.Formatter; import net.tfminecraft.simplefactions.utils.PostSettlementPayouts.PlayerUuidLookup; import net.tfminecraft.simplefactions.enums.FactionModifiers; +import net.tfminecraft.simplefactions.enums.GuildModifier; import net.tfminecraft.simplefactions.enums.Rules; import net.tfminecraft.simplefactions.government.proposal.TaxTarget; import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany; @@ -363,6 +365,9 @@ public double getIncome(Cashflow cashflow) { case SUPPLY_HUBS: amount = -SupplyHubService.dailyCost(guild); break; + case INFRASTRUCTURE_UPKEEP: + amount = -Math.max(0, GuildModifierOverride.resolve(guild, GuildModifier.INFRASTRUCTURE_UPKEEP)); + break; //Mercenary contracts case MERCENARY_CONTRACT: amount = getAggregatedContractEarnings(); @@ -556,6 +561,7 @@ public double getNetIncome() { case MILITARY_UPKEEP: case NODES: case SUPPLY_HUBS: + case INFRASTRUCTURE_UPKEEP: case PENALTIES: case GUILD_PAYMENTS: case OVERLORD_TAX: @@ -632,6 +638,7 @@ public double getDividendBase() { case MILITARY_UPKEEP: case NODES: case SUPPLY_HUBS: + case INFRASTRUCTURE_UPKEEP: case PENALTIES: case GUILD_PAYMENTS: case OVERLORD_TAX: @@ -1048,6 +1055,7 @@ private void applySettlementFor(Cashflow cf, DailyGuildTransfers buffer) { case NODES: // Supply hubs are a sink. Unpaid hubs were already removed, newest first. case SUPPLY_HUBS: + case INFRASTRUCTURE_UPKEEP: buffer.addExternalDelta(guild, getIncome(cf)); return; diff --git a/src/main/java/net/tfminecraft/simplefactions/loaders/BranchLoader.java b/src/main/java/net/tfminecraft/simplefactions/loaders/BranchLoader.java index ba6768a1..9585e925 100644 --- a/src/main/java/net/tfminecraft/simplefactions/loaders/BranchLoader.java +++ b/src/main/java/net/tfminecraft/simplefactions/loaders/BranchLoader.java @@ -14,6 +14,7 @@ import net.tfminecraft.simplefactions.guild.branch.Branch; import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.GuildType; public class BranchLoader { public static Map map = new HashMap<>(); @@ -38,12 +39,25 @@ public static Branch getByString(String id) { return null; } public static Branch getByGroup(Guild guild, int group) { + return getByGroup(guild.getType(), group); + } + public static Branch getByGroup(GuildType type, int group) { for(Branch b : getList()) { if(b.getGroup() != group) continue; - if(b.isAllowed(guild.getType())) return b; + if(b.isAllowed(type)) return b; } return null; } + public static void replaceDisallowedBranches(Map branches, GuildType type) { + for (Branch branch : branches.values()) { + if (!branch.isAllowed(type)) { + Branch replacement = getByGroup(type, branch.getGroup()); + if (replacement != null) { + branches.put(branch.getGroup(), new Branch(replacement, branch.getLevel())); + } + } + } + } public void load(File configFile) { FileConfiguration config = new YamlConfiguration(); try { diff --git a/src/main/resources/Guilds/branches.yml b/src/main/resources/Guilds/branches.yml index 475f04c3..dff85d27 100644 --- a/src/main/resources/Guilds/branches.yml +++ b/src/main/resources/Guilds/branches.yml @@ -17,6 +17,7 @@ bureaucracy: - trade_power 1 0.5 - production 4 0.5 - trade_upkeep 0 0.03 + - infrastructure 0 0.1 - prestige 0 5 - prestige_bonus 0 0.5 guild_halls: @@ -78,12 +79,31 @@ supply_lines: - "#d4c9aeEach hub costs more to run." allowed-types: - guild - - realm modifiers: - hub_limit 2 0.5 - hub_trade 0 0.05 - hub_production 0 0.08 - - hub_upkeep 2 0.5 + - hub_upkeep 1 0.5 + - trade_upkeep 0 0.01 + - prestige 0 5 +infrastructure: + name: "#86d1b0Infrastructure" + icon: "rail.0" + group: 3 + description: + - "#86d1b0Infrastructure #d4c9aeimproves poor" + - "#d4c9aeland in the realm for every guild," + - "#d4c9aestarting at the capital. Hubs come" + - "#d4c9aemore slowly; each level costs daily upkeep." + allowed-types: + - realm + modifiers: + - infrastructure 0 2 + - infrastructure_upkeep 0 1 + - hub_limit 2 0.25 + - hub_trade 0 0.05 + - hub_production 0 0.08 + - hub_upkeep 1 0.5 - trade_upkeep 0 0.01 - prestige 0 5 counting_houses: diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java index 834ce31e..e755bdd9 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/hub/SupplyHubServiceTest.java @@ -30,6 +30,7 @@ import net.tfminecraft.simplefactions.database.JsonUtil; import net.tfminecraft.simplefactions.database.SupplyHubData; import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.branch.Branch; import net.tfminecraft.simplefactions.enums.GuildModifier; import net.tfminecraft.simplefactions.enums.Rules; import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.BuildFailure; @@ -422,6 +423,57 @@ void ledger_supplyHubsIsItsOwnSinkLine() { assertEquals(-30.0, ledger.getNetIncome(), 1e-9); } + @Test + void infrastructureHubLimitFloorsTheFractionalLevels() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("group", 3); + yaml.set("modifiers", List.of("HUB_LIMIT 2 0.25")); + Branch prototype = new Branch("infrastructure", yaml); + for (int level : List.of(0, 3, 4, 8)) { + Guild guild = mock(Guild.class); + when(guild.getModifier(GuildModifier.HUB_LIMIT)) + .thenReturn(new Branch(prototype, level).getAmount(GuildModifier.HUB_LIMIT)); + int expected = level < 4 ? 2 : level < 8 ? 3 : 4; + assertEquals(expected, SupplyHubService.limit(guild)); + } + } + + @Test + void supplyLinesHubLimitAddsOneHubEveryTwoLevels() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("group", 3); + yaml.set("modifiers", List.of("HUB_LIMIT 2 0.5")); + Branch prototype = new Branch("supply_lines", yaml); + for (int level : List.of(0, 2, 4)) { + Guild guild = mock(Guild.class); + when(guild.getModifier(GuildModifier.HUB_LIMIT)) + .thenReturn(new Branch(prototype, level).getAmount(GuildModifier.HUB_LIMIT)); + assertEquals(2 + level / 2, SupplyHubService.limit(guild)); + } + } + + @Test + void ledger_infrastructureUpkeepIsShownAndSettledWithoutShedding() { + Guild guild = mock(Guild.class); + Faction faction = mock(Faction.class); + Bank bank = mock(Bank.class); + when(bank.getWealth()).thenReturn(0.0); + when(guild.getModifier(GuildModifier.INFRASTRUCTURE_UPKEEP)).thenReturn(4.0); + Ledger ledger = ledger(faction, guild, bank); + + assertTrue(Cashflow.INFRASTRUCTURE_UPKEEP.getDisplay().contains("Infrastructure")); + assertEquals(-4.0, ledger.getIncome(Cashflow.INFRASTRUCTURE_UPKEEP), 1e-9); + assertEquals(-4.0, ledger.getNetIncome(), 1e-9); + DailyGuildTransfers buffer = new DailyGuildTransfers(); + ledger.populateDailyTransfers(buffer); + assertEquals(-4.0, buffer.getExternalDeltas().get(guild), 1e-9); + + Guild withoutInfrastructure = mock(Guild.class); + when(withoutInfrastructure.getModifier(GuildModifier.INFRASTRUCTURE_UPKEEP)).thenReturn(0.0); + Ledger noInfrastructureLedger = ledger(faction, withoutInfrastructure, bank); + assertEquals(0.0, noInfrastructureLedger.getIncome(Cashflow.INFRASTRUCTURE_UPKEEP), 1e-9); + } + @Test void ledger_removesEveryHubWhenNoneCanBePaid() { Guild guild = mock(Guild.class); diff --git a/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java b/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java index f9194797..39ffa80a 100644 --- a/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/guild/income/BranchIncomePreviewTest.java @@ -173,7 +173,7 @@ void concurrentPreviewsStayOnTheirOwnModifiers() throws Exception { } @Test - void branchPreviewUsesHypotheticalHubTradeLevel() { + void branchPreviewUsesHypotheticalHubTradeLevel() { HubNetwork.setLinksForTests(Map.of("fields", List.of( new Link(capital.getId(), neighbour.getId(), Mode.RAIL, 0, 0.9, 0)))); YamlConfiguration yaml = new YamlConfiguration(); @@ -183,6 +183,19 @@ void branchPreviewUsesHypotheticalHubTradeLevel() { Branch supplyLines = new Branch(new Branch("supply_lines", yaml), 0); assertTrue(live.previewUpgradeIncomeExact(guild, supplyLines) > 0); - assertEquals(0, supplyLines.getLevel()); - } + assertEquals(0, supplyLines.getLevel()); + } + + @Test + void branchPreviewIncludesInfrastructureUpkeepChange() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("group", 3); + yaml.set("modifiers", List.of("INFRASTRUCTURE_UPKEEP 0 1")); + Branch infrastructure = new Branch(new Branch("infrastructure", yaml), 2); + when(guild.getModifier(GuildModifier.INFRASTRUCTURE_UPKEEP)).thenReturn(2.0); + + assertEquals(-1.0, live.previewUpgradeIncomeExact(guild, infrastructure)); + assertEquals(1.0, live.previewDowngradeIncomeExact(guild, infrastructure)); + assertEquals(2, infrastructure.getLevel()); + } } diff --git a/src/test/java/net/tfminecraft/simplefactions/loaders/SupplyHubBranchesTest.java b/src/test/java/net/tfminecraft/simplefactions/loaders/SupplyHubBranchesTest.java index 348e34b0..46373845 100644 --- a/src/test/java/net/tfminecraft/simplefactions/loaders/SupplyHubBranchesTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/loaders/SupplyHubBranchesTest.java @@ -39,6 +39,7 @@ void bundledBranchesLoadForGuildsAndRealms() { YamlConfiguration types = new YamlConfiguration(); types.set("guild.name", "Guild"); types.set("realm.name", "Realm"); + types.set("realm.base", true); GuildLoader.map.put("guild", new GuildType("guild", types.getConfigurationSection("guild"))); GuildLoader.map.put("realm", new GuildType("realm", types.getConfigurationSection("realm"))); @@ -63,21 +64,28 @@ void bundledBranchesLoadForGuildsAndRealms() { assertNotNull(freightGuild); assertNotNull(freightRealm); assertEquals("supply_lines", supplyGuild.getId()); + assertEquals("infrastructure", supplyRealm.getId()); assertEquals("counting_houses", freightRealm.getId()); assertEquals(2.0, supplyGuild.getModifier(GuildModifier.HUB_LIMIT).getBase()); assertEquals(0.5, supplyGuild.getModifier(GuildModifier.HUB_LIMIT).getPerLevel()); assertEquals(0.05, supplyGuild.getModifier(GuildModifier.HUB_TRADE).getPerLevel()); assertEquals(0.08, supplyGuild.getModifier(GuildModifier.HUB_PRODUCTION).getPerLevel()); assertEquals(-0.01, freightRealm.getModifier(GuildModifier.TRADE_UPKEEP).getPerLevel()); - assertEquals(2.0, supplyGuild.getModifier(GuildModifier.HUB_UPKEEP).getBase()); + assertEquals(1.0, supplyGuild.getModifier(GuildModifier.HUB_UPKEEP).getBase()); assertEquals(0.5, supplyGuild.getModifier(GuildModifier.HUB_UPKEEP).getPerLevel()); + Branch infrastructure = new Branch(BranchLoader.getByString("infrastructure"), 10); + assertEquals(20.0, infrastructure.getAmount(GuildModifier.INFRASTRUCTURE)); + assertEquals(10.0, infrastructure.getAmount(GuildModifier.INFRASTRUCTURE_UPKEEP)); + Branch bureaucracy = new Branch(BranchLoader.getByString("bureaucracy"), 5); + assertEquals(20.5, infrastructure.getAmount(GuildModifier.INFRASTRUCTURE) + + bureaucracy.getAmount(GuildModifier.INFRASTRUCTURE)); Branch storehouses = new Branch(BranchLoader.getByString("storehouses"), 3); Branch supplyLines = new Branch(BranchLoader.getByString("supply_lines"), 4); assertEquals(0.70, storehouses.getAmount(GuildModifier.TRADE_CARRY)); assertEquals(0.09, storehouses.getAmount(GuildModifier.HUB_PRODUCTION)); assertNull(storehouses.getModifier(GuildModifier.HUB_UPKEEP)); - assertEquals(2.0, supplyGuild.getAmount(GuildModifier.HUB_UPKEEP)); - assertEquals(4.0, supplyLines.getAmount(GuildModifier.HUB_UPKEEP)); + assertEquals(1.0, supplyGuild.getAmount(GuildModifier.HUB_UPKEEP)); + assertEquals(3.0, supplyLines.getAmount(GuildModifier.HUB_UPKEEP)); Branch twoStorehouses = new Branch(BranchLoader.getByString("storehouses"), 2); Branch fiveSupplyLines = new Branch(BranchLoader.getByString("supply_lines"), 5); assertEquals(0.46, twoStorehouses.getAmount(GuildModifier.HUB_PRODUCTION) @@ -85,6 +93,48 @@ void bundledBranchesLoadForGuildsAndRealms() { assertTrue(!supplyGuild.getDescription().isEmpty()); } + @Test + void savedBranchesMoveToTheAllowedBranchAtTheSameLevel() { + loadBundledBranches(); + Branch supplyLines = new Branch(BranchLoader.getByString("supply_lines"), 3); + Map realmBranches = new HashMap<>(Map.of(3, supplyLines)); + Map normalBranches = new HashMap<>(Map.of(3, supplyLines)); + BranchLoader.replaceDisallowedBranches(realmBranches, GuildLoader.getByString("realm")); + BranchLoader.replaceDisallowedBranches(normalBranches, GuildLoader.getByString("guild")); + + assertEquals("infrastructure", realmBranches.get(3).getId()); + assertEquals(3, realmBranches.get(3).getLevel()); + assertEquals("supply_lines", normalBranches.get(3).getId()); + assertEquals(3, normalBranches.get(3).getLevel()); + + YamlConfiguration legacyConfig = new YamlConfiguration(); + legacyConfig.set("name", "Supply Lines"); + legacyConfig.set("group", 3); + legacyConfig.set("allowed-types", java.util.List.of("guild", "realm")); + BranchLoader.map.put("supply_lines", new Branch("supply_lines", legacyConfig)); + Map legacyRealmBranches = new HashMap<>(Map.of(3, + new Branch(BranchLoader.getByString("supply_lines"), 0))); + BranchLoader.replaceDisallowedBranches(legacyRealmBranches, GuildLoader.getByString("realm")); + assertEquals("supply_lines", legacyRealmBranches.get(3).getId()); + assertEquals(0, legacyRealmBranches.get(3).getLevel()); + } + + private static void loadBundledBranches() { + YamlConfiguration types = new YamlConfiguration(); + types.set("guild.name", "Guild"); + types.set("realm.name", "Realm"); + types.set("realm.base", true); + GuildLoader.map.clear(); + GuildLoader.map.put("guild", new GuildType("guild", types.getConfigurationSection("guild"))); + GuildLoader.map.put("realm", new GuildType("realm", types.getConfigurationSection("realm"))); + YamlConfiguration config = YamlConfiguration.loadConfiguration( + new File("src/main/resources/Guilds/branches.yml")); + BranchLoader.map.clear(); + for (String key : config.getKeys(false)) { + BranchLoader.map.put(key, new Branch(key, config.getConfigurationSection(key))); + } + } + @Test void freightYardsResolvesToCountingHousesOnlyWhenNoExactBranchExists() { Branch counting = new Branch("counting_houses", new YamlConfiguration().createSection("counting_houses"));