Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ public enum GuildModifier {
TRADE_UPKEEP("#d6645aTrade Upkeep", false),
PRODUCTION("#f2c94cProduction", true),
TRADE_CARRY("#86d1b0Trade Carry", true),
HUB_LIMIT("#c99b70Supply Hub Limit", true),
HUB_TRADE("#b5835aHub Trade Transfer", true),
HUB_PRODUCTION("#a9744fHub Production Transfer", true),
DIPLOMATIC_CAPACITY("#56ccf2Diplomatic Capacity", true),
ADMIN_POWER("#ebde54Administrative Power", true),
ADMIN_POWER_GAIN("#d1b347Administrative Power Gain", true),
Expand All @@ -32,4 +35,3 @@ public boolean isPositive() {
return positive;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.SimpleFactions;
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.guild.GuildModifierOverride;
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Link;
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Mode;
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Rates;
Expand All @@ -22,6 +23,7 @@
import net.tfminecraft.simplefactions.managers.ProvinceManager;
import net.tfminecraft.simplefactions.map.SeaConnectivity;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.enums.GuildModifier;

/**
* The connections between each guild's active supply hubs.
Expand Down Expand Up @@ -101,14 +103,15 @@ public static double potentialTrade(
if (guild == null || target == null || provinces == null) {
return 0;
}
double bonus = GuildModifierOverride.resolve(guild, GuildModifier.HUB_TRADE);
double best = 0;
for (Installation source : activeSites(guild, SupplyHubService.allGuilds())) {
Link link = connect(source, target, provinces);
if (link == null) {
continue;
}
double power = provinces.get(source.getProvince()).getRawGuildTrade(guild);
best = Math.max(best, power * link.tradeFactor());
best = Math.max(best, power * link.boostedTradeFactor(bonus));
}
return best;
}
Expand Down Expand Up @@ -176,6 +179,9 @@ private static List<Installation> activeSites(Guild guild, List<Guild> guilds) {
List<Installation> sites = new ArrayList<>();
String guildFactionId = guild.getFaction() == null ? null : guild.getFaction().getId();
for (SupplyHub hub : guild.getSupplyHubs()) {
if (SupplyHubService.beyondGuildLimit(guild, hub)) {
continue;
}
Installation installation =
SupplyHubService.findInstallation(hub.ownerFactionId(), hub.installationId());
if (installation == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ public record Link(
double distance,
double tradeFactor,
double productionFactor) {

/** The boosted share is capped before the cached distance loss is applied. */
public double boostedTradeFactor(double bonus) {
return boostedFactor(tradeFactor, mode, distance, bonus, true);
}

/** The boosted share is capped before the cached distance loss is applied. */
public double boostedProductionFactor(double bonus) {
return boostedFactor(productionFactor, mode, distance, bonus, false);
}
}

private static final Map<Mode, Rates> DEFAULTS = new EnumMap<>(Mode.class);
Expand Down Expand Up @@ -143,6 +153,21 @@ public static Link link(int fromProvince, int toProvince, Mode mode, double dist
delivered(modeRates.production(), modeRates.keptPer1000(), distance));
}

private static double boostedFactor(double factor, Mode mode, double distance, double bonus, boolean trade) {
Rates modeRates = rates(mode);
double baseShare = trade ? modeRates.trade() : modeRates.production();
if (factor <= 0 || baseShare <= 0) {
return 0;
}
double distanceLoss = delivered(1.0, modeRates.keptPer1000(), distance);
if (distanceLoss <= 0) {
return 0;
}
double unboostedShare = Math.min(MAX_SHARE, factor / distanceLoss);
double boostedShare = Math.min(MAX_SHARE, unboostedShare * (1.0 + Math.max(0, bonus)));
return delivered(boostedShare, modeRates.keptPer1000(), distance);
}

private static double share(double value) {
if (!Double.isFinite(value) || value < 0) {
return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.SimpleFactions;
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.guild.GuildModifierOverride;
import net.tfminecraft.simplefactions.enums.GuildModifier;
import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.BuildFailure;
import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.HubStanding;
import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.PlacedCandidate;
Expand Down Expand Up @@ -208,15 +210,15 @@ private static boolean build(Player player) {
chosen.hubSlots(),
SupplyHubService.hasHub(guild.getSupplyHubs(), chosen.ownerFactionId(), installation.getId()),
guild.getSupplyHubs().size(),
SupplyHubService.baseLimit(),
SupplyHubService.limit(guild),
SupplyHubService.countLoaded(chosen.ownerFactionId(), installation.getId()),
Math.max(
tradeInProvince(installation.getProvince(), guild.getId()),
reachableTrade(guild, installation)),
SupplyHubService.ownerAllows(guildFactionId, chosen.ownerFactionId(), permit));
if (failure != null) {
player.sendMessage(SupplyHubService.buildFailureMessage(
failure, installation.getKind().getDisplayName(), SupplyHubService.baseLimit()));
failure, installation.getKind().getDisplayName(), SupplyHubService.limit(guild)));
return true;
}
guild.getSupplyHubs().add(new SupplyHub(
Expand All @@ -233,11 +235,11 @@ private static boolean list(Player player) {
return true;
}
List<SupplyHub> hubs = SupplyHubService.oldestFirst(guild.getSupplyHubs());
player.sendMessage("搂eSupply hubs 搂7(" + hubs.size() + "/" + SupplyHubService.limit(guild) + ")");
if (hubs.isEmpty()) {
player.sendMessage("搂7Your guild has no supply hubs");
return true;
}
player.sendMessage("搂eSupply hubs 搂7(" + hubs.size() + ")");
for (SupplyHub hub : hubs) {
Installation installation = SupplyHubService.findInstallation(
hub.ownerFactionId(), hub.installationId());
Expand Down Expand Up @@ -300,6 +302,10 @@ private static void listPermits(Player player, Faction faction) {
}

private static String statusOf(Guild guild, SupplyHub hub, Installation installation) {
if (SupplyHubService.beyondGuildLimit(guild, hub)) {
return SupplyHubService.statusText(new HubStanding(
false, SupplyHubService.DormantReason.BEYOND_GUILD_LIMIT));
}
String guildFactionId = guild.getFaction() == null ? null : guild.getFaction().getId();
Faction owner = FactionManager.getByString(hub.ownerFactionId());
boolean permit = owner != null && owner.hasHubPermit(guild.getId());
Expand Down Expand Up @@ -385,15 +391,19 @@ private static List<String> connectionLines(Guild guild, Installation installati
if (installation == null) {
return lines;
}
double tradeBonus = GuildModifierOverride.resolve(guild, GuildModifier.HUB_TRADE);
double productionBonus = GuildModifierOverride.resolve(guild, GuildModifier.HUB_PRODUCTION);
for (HubTransport.Link link : HubNetwork.linksFor(guild)) {
if (link.fromProvince() != installation.getProvince()
|| link.mode().getKind() != installation.getKind()) {
continue;
}
lines.add("搂7Sends to 搂f" + hubNameIn(guild, link) + " 搂7by " + link.mode().getKey()
+ " (" + Math.round(link.distance()) + " blocks): 搂e"
+ Math.round(link.tradeFactor() * 100) + "% 搂7trade, 搂e"
+ Math.round(link.productionFactor() * 100) + "% 搂7production");
+ Math.round(link.boostedTradeFactor(tradeBonus) * 100)
+ "% 搂7trade, 搂e"
+ Math.round(link.boostedProductionFactor(productionBonus) * 100)
+ "% 搂7production");
}
return lines;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import net.tfminecraft.simplefactions.Cache;
import net.tfminecraft.simplefactions.database.SupplyHubData;
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.guild.GuildModifierOverride;
import net.tfminecraft.simplefactions.enums.GuildModifier;
import net.tfminecraft.simplefactions.installation.Installation;
import net.tfminecraft.simplefactions.installation.InstallationKind;
import net.tfminecraft.simplefactions.managers.FactionManager;
Expand Down Expand Up @@ -48,7 +50,8 @@ public enum BuildFailure {
public enum DormantReason {
INSTALLATION_GONE,
NO_PERMIT,
BEYOND_HUB_SLOTS
BEYOND_HUB_SLOTS,
BEYOND_GUILD_LIMIT
}

public record HubStanding(boolean active, DormantReason reason) {
Expand Down Expand Up @@ -104,6 +107,23 @@ public static double upkeepGrowth() {
return Cache.supplyHubUpkeepGrowth;
}

public static int limit(Guild guild) {
if (guild == null) {
return baseLimit();
}
return Math.max(0, baseLimit() + (int) Math.floor(
GuildModifierOverride.resolve(guild, GuildModifier.HUB_LIMIT)));
}

/** Hubs above the guild limit are dormant newest first, and remain in the saved list. */
public static boolean beyondGuildLimit(Guild guild, SupplyHub hub) {
if (guild == null || hub == null) {
return false;
}
List<SupplyHub> ordered = oldestFirst(guild.getSupplyHubs());
return indexOfHub(ordered, hub) >= limit(guild);
}

/**
* Table used when {@code hub-slots} is absent for that kind and level.
* Anything not listed is 0.
Expand Down Expand Up @@ -262,6 +282,8 @@ public static String dormantReasonText(DormantReason reason) {
return "the owning faction has not granted a hub permit";
case BEYOND_HUB_SLOTS:
return "it is beyond this installation's hub slots";
case BEYOND_GUILD_LIMIT:
return "it is beyond your guild's supply hub limit";
default:
return "dormant";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,21 +167,22 @@ private void carryTradeThroughHubs(Guild guild) {
ProvinceDataEntry entry = province.getAllData().get(guild.getId());
if (entry != null) walked.put(province.getId(), entry.getDistance());
}
deliverTradeThroughHubs(guild, links);
deliverTradeThroughHubs(
guild, links, GuildModifierOverride.resolve(guild, GuildModifier.HUB_TRADE));
for (Map.Entry<Integer, Integer> distance : walked.entrySet()) {
ProvinceDataEntry entry = provinces.get(distance.getKey()).getAllData().get(guild.getId());
if (entry != null) entry.setDistance(distance.getValue());
}
}

private void deliverTradeThroughHubs(Guild guild, List<Link> links) {
private void deliverTradeThroughHubs(Guild guild, List<Link> links, double bonus) {
for (int pass = 0; pass <= links.size(); pass++) {
boolean moved = false;
for (Link link : links) {
Province from = provinces.get(link.fromProvince());
Province to = provinces.get(link.toProvince());
if (from == null || to == null) continue;
double delivered = from.getRawGuildTrade(guild) * link.tradeFactor();
double delivered = from.getRawGuildTrade(guild) * link.boostedTradeFactor(bonus);
if (delivered < 0.5 || delivered <= to.getRawGuildTrade(guild)) continue;
to.seedTrade(this, guild, delivered);
moved = true;
Expand All @@ -192,13 +193,14 @@ private void deliverTradeThroughHubs(Guild guild, List<Link> links) {

private void carryProductionThroughHubs(Guild guild) {
List<Link> links = HubNetwork.linksFor(guild);
double bonus = GuildModifierOverride.resolve(guild, GuildModifier.HUB_PRODUCTION);
for (int pass = 0; pass <= links.size(); pass++) {
boolean moved = false;
for (Link link : links) {
Province from = provinces.get(link.fromProvince());
Province to = provinces.get(link.toProvince());
if (from == null || to == null) continue;
double delivered = from.getGuildProduction(guild) * link.productionFactor();
double delivered = from.getGuildProduction(guild) * link.boostedProductionFactor(bonus);
if (delivered < 0.1 || delivered <= to.getGuildProduction(guild)) continue;
to.seedProduction(this, guild, delivered);
moved = true;
Expand Down
33 changes: 33 additions & 0 deletions src/main/resources/Guilds/branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,36 @@ storehouses:
- trade_carry 0.25
- trade_upkeep 0 0.02
- prestige 0 5
supply_lines:
name: "#b5835aSupply Lines"
icon: "rail.0"
group: 3
description:
- "#b5835aSupply Lines #d4c9aestrengthen the"
- "#d4c9aeguild's network of supply hubs,"
- "#d4c9aemaking more hubs available and"
- "#d4c9aepassing more trade power between them."
allowed-types:
- guild
- realm
modifiers:
- hub_limit 0 0.5
- hub_trade 0 0.03
- trade_upkeep 0 0.01
- prestige 0 5
freight_yards:
name: "#a9744fFreight Yards"
icon: "chest_minecart.0"
group: 4
description:
- "#a9744fFreight Yards #d4c9aeimprove the"
- "#d4c9aehandover of production between"
- "#d4c9aehubs, helping workshops prosper"
- "#d4c9aefar beyond the guild's capital."
allowed-types:
- guild
- realm
modifiers:
- hub_production 0 0.08
- trade_upkeep 0 0.01
- prestige 0 5
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ void linkCarriesBothFactors() {
assertEquals(0.30 * 0.85, link.productionFactor(), 1e-9);
}

@Test
void modifierBoostIsAppliedBeforeTheShareCapAndDistanceLoss() {
Link link = HubTransport.link(3, 9, Mode.RAIL, 1000);

assertEquals(0.70 * 1.3 * 0.90, link.boostedTradeFactor(0.30), 1e-9);
assertEquals(0.25 * 1.3 * 0.90, link.boostedProductionFactor(0.30), 1e-9);

YamlConfiguration config = new YamlConfiguration();
config.set("supply-hubs.transport.rail.trade", 0.90);
HubTransport.loadConfig(config);
Link capped = HubTransport.link(3, 9, Mode.RAIL, 0);
assertEquals(HubTransport.MAX_SHARE, capped.boostedTradeFactor(0.5), 1e-9);
}

@Test
void maxRangeZeroMeansNoLimit() {
assertTrue(HubTransport.inRange(HubTransport.rates(Mode.RAIL), 50_000));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import net.tfminecraft.simplefactions.database.JsonUtil;
import net.tfminecraft.simplefactions.database.SupplyHubData;
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.enums.GuildModifier;
import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.BuildFailure;
import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.DormantReason;
import net.tfminecraft.simplefactions.guild.hub.SupplyHubService.HubCandidate;
Expand Down Expand Up @@ -187,6 +188,27 @@ void standing_eachDormantReason_andActive() {
assertTrue(SupplyHubService.statusText(noPermit).contains("hub permit"));
}

@Test
void guildLimitIncludesWholeBranchBonus_andExcessHubsAreNewestFirst() {
Cache.supplyHubBaseLimit = 2;
Guild guild = mock(Guild.class);
List<SupplyHub> hubs = new ArrayList<>(List.of(
hub("rome", "old", 1), hub("rome", "middle", 2), hub("rome", "new", 3)));
when(guild.getSupplyHubs()).thenReturn(hubs);
when(guild.getModifier(GuildModifier.HUB_LIMIT)).thenReturn(1.5);

assertEquals(3, SupplyHubService.limit(guild));
assertFalse(SupplyHubService.beyondGuildLimit(guild, hubs.get(0)));
assertFalse(SupplyHubService.beyondGuildLimit(guild, hubs.get(1)));
assertFalse(SupplyHubService.beyondGuildLimit(guild, hubs.get(2)));

when(guild.getModifier(GuildModifier.HUB_LIMIT)).thenReturn(0.5);
assertEquals(2, SupplyHubService.limit(guild));
assertTrue(SupplyHubService.beyondGuildLimit(guild, hubs.get(2)));
assertTrue(SupplyHubService.statusText(new HubStanding(
false, DormantReason.BEYOND_GUILD_LIMIT)).contains("guild's supply hub limit"));
}

@Test
void dormantHub_stillCostsUpkeepAndOccupiesASlot() {
SupplyHub first = hub("rome", "harbour", 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.guild.GuildModifierOverride;
import net.tfminecraft.simplefactions.guild.branch.Branch;
import net.tfminecraft.simplefactions.guild.hub.HubNetwork;
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Link;
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Mode;
import net.tfminecraft.simplefactions.managers.FactionManager;
import net.tfminecraft.simplefactions.managers.ProvinceManager;
import net.tfminecraft.simplefactions.map.provinces.Province;
Expand Down Expand Up @@ -89,6 +92,7 @@ void setUp() {
@AfterEach
void tearDown() {
GuildModifierOverride.clear();
HubNetwork.setLinksForTests(null);
Cache.provincesEnabled = savedProvincesEnabled;
if (savedPlainsCarry == null) {
Cache.tradeCarry.remove(Terrain.PLAINS);
Expand Down Expand Up @@ -165,4 +169,18 @@ void concurrentPreviewsStayOnTheirOwnModifiers() throws Exception {
assertEquals(2, branch.getLevel());
assertEquals(12.5, capital.getProsperity());
}

@Test
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();
yaml.set("name", "Supply Lines");
yaml.set("group", 3);
yaml.set("modifiers", List.of("HUB_TRADE 0 0.2"));
Branch supplyLines = new Branch(new Branch("supply_lines", yaml), 0);

assertTrue(live.previewUpgradeIncomeExact(guild, supplyLines) > 0);
assertEquals(0, supplyLines.getLevel());
}
}
Loading
Loading