diff --git a/src/main/java/net/tfminecraft/simplefactions/diplomacy/RelationType.java b/src/main/java/net/tfminecraft/simplefactions/diplomacy/RelationType.java index 122a991a..3eba778c 100644 --- a/src/main/java/net/tfminecraft/simplefactions/diplomacy/RelationType.java +++ b/src/main/java/net/tfminecraft/simplefactions/diplomacy/RelationType.java @@ -29,6 +29,7 @@ public class RelationType { private String link; private double baseCost; + private double blocScaling; private List giveModifiers = new ArrayList<>(); private List recieveModifiers = new ArrayList<>(); @@ -51,6 +52,7 @@ public RelationType(String key, ConfigurationSection config) { name = StringFormatter.formatHex(config.getString("name", "None")); prefix = StringFormatter.formatHex(config.getString("prefix", "#a89977Our ")); baseCost = config.getDouble("cost", 0.0); + blocScaling = Math.max(0.0, config.getDouble("bloc-scaling", 0.0)); target = config.getInt("target", 0); limit = config.getInt("limit", -1); def = config.getBoolean("default", false); @@ -116,6 +118,11 @@ public boolean canPickForWar() { public double getBaseCost() { return baseCost; } + + /** How strongly the cost grows with the two factions' share of all prestige; 0 turns it off. */ + public double getBlocScaling() { + return blocScaling; + } public boolean hasThreshold() { return threshold != null; diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/RelationManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/RelationManager.java index 662d886f..0849b176 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/RelationManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/RelationManager.java @@ -72,6 +72,9 @@ public static double getDiplomaticCost(Faction from, Faction to, RelationType r) if(r.isVassalage()) { cost/=3.0; } + if(cost != 0 && r.getBlocScaling() > 0) { + cost *= 1 + r.getBlocScaling() * blocShare(from, to); + } return cost; } @@ -95,6 +98,23 @@ private static double prestigeCostScale(double prestige) { return Math.sqrt(nonNegativePrestige); } + /** The share of all factions' prestige that these two hold together, from 0 to 1. */ + static double blocShare(Faction from, Faction to) { + double total = 0; + for (Faction faction : FactionManager.factions) { + if (faction == null) continue; + total += Math.max(0, prestigeOf(faction)); + } + if (total <= 0) { + return 0; + } + double bloc = Math.max(0, prestigeOf(from)); + if (from != to) { + bloc += Math.max(0, prestigeOf(to)); + } + return Math.min(1, bloc / total); + } + private static double prestigeOf(Faction faction) { Double prestige = faction.getPrestige(); return prestige == null ? 0 : prestige; diff --git a/src/main/resources/diplomacy.yml b/src/main/resources/diplomacy.yml index b4a47807..1fa4d82f 100644 --- a/src/main/resources/diplomacy.yml +++ b/src/main/resources/diplomacy.yml @@ -82,7 +82,10 @@ types: ally: name: "#975bbdAlly" target: 30 - cost: 3.5 + cost: 1.5 + # Multiplies the cost by 1 + bloc-scaling * (both factions' prestige / all factions' prestige), + # so an alliance between the strongest factions costs far more than one between small ones. + bloc-scaling: 8 mutual: true #Do both parties need to agree to this relationship? threshold: mode: higher_than_or_equal_to diff --git a/src/test/java/net/tfminecraft/simplefactions/loaders/RelationLoaderWarPickableTest.java b/src/test/java/net/tfminecraft/simplefactions/loaders/RelationLoaderWarPickableTest.java index d748017f..94c7dd10 100644 --- a/src/test/java/net/tfminecraft/simplefactions/loaders/RelationLoaderWarPickableTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/loaders/RelationLoaderWarPickableTest.java @@ -54,6 +54,25 @@ void missingCanPickForWar_defaultsTrue() throws Exception { assertTrue(type.isVassalage()); } + @Test + void blocScaling_isReadAndDefaultsToOff() throws Exception { + YamlConfiguration config = new YamlConfiguration(); + config.loadFromString(""" + name: Ally + cost: 1.5 + bloc-scaling: 8 + """); + assertEquals(8.0, new RelationType("ally", config).getBlocScaling(), 1e-9); + + YamlConfiguration plain = new YamlConfiguration(); + plain.loadFromString(""" + name: Subject + bloc-scaling: -3 + """); + assertEquals(0.0, new RelationType("subject", plain).getBlocScaling(), 1e-9); + assertEquals(0.0, new RelationType("none", new YamlConfiguration()).getBlocScaling(), 1e-9); + } + @Test void canPickForWarFalse_isNotPickable() throws Exception { YamlConfiguration config = new YamlConfiguration(); diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/RelationManagerDiplomaticCostTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/RelationManagerDiplomaticCostTest.java index 3feca9a7..1e9620b2 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/RelationManagerDiplomaticCostTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/RelationManagerDiplomaticCostTest.java @@ -1,9 +1,15 @@ package net.tfminecraft.simplefactions.managers; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import net.tfminecraft.simplefactions.diplomacy.Attitude; @@ -12,6 +18,20 @@ class RelationManagerDiplomaticCostTest { + private List previousFactions; + + @BeforeEach + void clearFactions() { + previousFactions = new ArrayList<>(FactionManager.factions); + FactionManager.factions.clear(); + } + + @AfterEach + void restoreFactions() { + FactionManager.factions.clear(); + FactionManager.factions.addAll(previousFactions); + } + @Test void relationCost_usesDiminishingReturnsAbove100Prestige() { Faction origin = mock(Faction.class); @@ -55,4 +75,66 @@ void zeroAttitudeCost_isZero() { when(attitude.getBaseCost()).thenReturn(0.0); assertEquals(0.0, RelationManager.getDiplomaticCost(origin, target, attitude), 1e-9); } + + @Test + void blocScaling_raisesCostByTheBlocsShareOfAllPrestige() { + Faction origin = factionWithPrestige(300.0); + Faction target = factionWithPrestige(900.0); + factionWithPrestige(1200.0); + RelationType type = allianceType(1.5, 8.0); + // The two hold 1200 of 2400 prestige, so the multiplier is 1 + 8 * 0.5. + assertEquals(1.5 * Math.sqrt(900.0) * 5.0, RelationManager.getDiplomaticCost(origin, target, type), 1e-9); + } + + @Test + void blocScaling_chargesSmallBlocsLessThanLargeOnes() { + Faction small = factionWithPrestige(400.0); + Faction alsoSmall = factionWithPrestige(400.0); + Faction large = factionWithPrestige(3000.0); + Faction alsoLarge = factionWithPrestige(3000.0); + RelationType type = allianceType(1.5, 8.0); + double smallCost = RelationManager.getDiplomaticCost(small, alsoSmall, type); + double largeCost = RelationManager.getDiplomaticCost(large, alsoLarge, type); + // Without bloc scaling the large pair would pay sqrt(3000/400) times more; with it they pay more still. + assertTrue(largeCost / smallCost > Math.sqrt(3000.0 / 400.0) * 2); + } + + @Test + void blocScaling_isIgnoredForRelationsWithoutIt() { + Faction origin = factionWithPrestige(300.0); + Faction target = factionWithPrestige(900.0); + RelationType type = allianceType(2.0, 0.0); + assertEquals(2.0 * Math.sqrt(900.0), RelationManager.getDiplomaticCost(origin, target, type), 1e-9); + } + + @Test + void blocShare_ignoresNegativePrestigeAndNeverExceedsOne() { + Faction origin = factionWithPrestige(500.0); + Faction target = factionWithPrestige(500.0); + factionWithPrestige(-200.0); + assertEquals(1.0, RelationManager.blocShare(origin, target), 1e-9); + } + + @Test + void blocShare_isZeroWhenNoFactionHasPrestige() { + Faction origin = factionWithPrestige(0.0); + Faction target = factionWithPrestige(0.0); + assertEquals(0.0, RelationManager.blocShare(origin, target), 1e-9); + } + + private static Faction factionWithPrestige(double prestige) { + Faction faction = mock(Faction.class); + when(faction.getPrestige()).thenReturn(prestige); + FactionManager.factions.add(faction); + return faction; + } + + private static RelationType allianceType(double baseCost, double blocScaling) { + RelationType type = mock(RelationType.class); + when(type.getBaseCost()).thenReturn(baseCost); + when(type.getBlocScaling()).thenReturn(blocScaling); + when(type.isSettable()).thenReturn(true); + when(type.isVassalage()).thenReturn(false); + return type; + } }