From f1b83c4da68c8031139c4ec64c0653851468fec9 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:46:34 +0000 Subject: [PATCH] Scale alliance cost by the bloc's share of all prestige An alliance now costs more the larger the two factions are together, measured as their share of every faction's prestige. Relation types opt in with bloc-scaling in diplomacy.yml; the default alliance uses cost 1.5 and bloc-scaling 8. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../diplomacy/RelationType.java | 7 ++ .../managers/RelationManager.java | 20 +++++ src/main/resources/diplomacy.yml | 5 +- .../RelationLoaderWarPickableTest.java | 19 +++++ .../RelationManagerDiplomaticCostTest.java | 82 +++++++++++++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) 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; + } }