From 88d42833e6bddce7d224015c7a9c5d45b7f54d32 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:47:16 +0000 Subject: [PATCH 1/2] Enlist the founder and count the main guild's company as offense A newly founded mercenary company now fills its first slot with the guild leader. The main guild's formed company adds its filled slots to the faction's manpower and shows as a fourth regiment in the military view. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../simplefactions/army/Military.java | 27 +++++- .../managers/inventory/MilitaryCreator.java | 18 ++++ .../managers/inventory/MilitaryView.java | 9 +- .../mercenary/company/MercenaryCompany.java | 12 +++ .../company/MercenaryCompanyService.java | 1 + .../mercenary/company/CompanyGuiTest.java | 1 + .../MercenaryCompanyFormationTest.java | 37 ++++++++ .../company/MercenaryHomeManpowerTest.java | 89 +++++++++++++++++++ .../mercenary/contract/ContractFixture.java | 2 + 9 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java diff --git a/src/main/java/net/tfminecraft/simplefactions/army/Military.java b/src/main/java/net/tfminecraft/simplefactions/army/Military.java index 90786702..380ea999 100644 --- a/src/main/java/net/tfminecraft/simplefactions/army/Military.java +++ b/src/main/java/net/tfminecraft/simplefactions/army/Military.java @@ -2,9 +2,12 @@ import java.util.ArrayList; import java.util.List; +import net.tfminecraft.simplefactions.guild.Guild; import net.tfminecraft.simplefactions.loaders.RegimentLoader; import net.tfminecraft.simplefactions.managers.FactionManager; import net.tfminecraft.simplefactions.managers.RelationManager; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany; +import net.tfminecraft.simplefactions.objects.handler.GuildHandler; import net.tfminecraft.simplefactions.objects.Faction; import net.tfminecraft.simplefactions.utils.Formatter; import net.tfminecraft.simplefactions.enums.FactionModifiers; @@ -140,8 +143,28 @@ public ExpandResult adminAdjustSlots(String regimentId, int delta) { return ExpandResult.ok(); } + /** + * The main guild's company, when it has finished founding. Only this company + * fights for the faction; companies of other guilds are hired by contract. + */ + public MercenaryCompany getHomeCompany() { + if(f == null) return null; + GuildHandler handler = f.getGuildHandler(); + if(handler == null) return null; + Guild main = handler.getGuild(f.getId()); + if(main == null) return null; + MercenaryCompany company = main.getCompany(); + return company != null && company.isFormed() ? company : null; + } + + /** Filled slots of the home company. Empty slots add nothing. */ + public int getMercenaryManpower() { + MercenaryCompany company = getHomeCompany(); + return company == null ? 0 : company.getFilledSlots(); + } + public int getManpower(boolean offense) { - int manpower = 0; + int manpower = getMercenaryManpower(); for(Regiment r : getRegiments()) { if(offense && !r.isOffensive()) continue; if(r.isLevy()) { @@ -156,7 +179,7 @@ public int getManpower(boolean offense) { } public int getManpowerNoLevy(boolean offense) { - int manpower = 0; + int manpower = getMercenaryManpower(); for(Regiment r : regiments) { if(offense && !r.isOffensive()) continue; if(r.isLevy()) continue; diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryCreator.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryCreator.java index 7e46e03a..053ecc25 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryCreator.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryCreator.java @@ -15,6 +15,7 @@ import net.tfminecraft.simplefactions.army.Military; import net.tfminecraft.simplefactions.army.MilitaryExpansion; import net.tfminecraft.simplefactions.army.Regiment; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany; import net.tfminecraft.simplefactions.objects.Faction; import net.tfminecraft.simplefactions.utils.Formatter; import net.tfminecraft.simplefactions.enums.FactionModifiers; @@ -122,6 +123,23 @@ public ItemStack createRegimentIcon(Faction f, Regiment r) { } // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") + public ItemStack createCompanyRegimentIcon(MercenaryCompany company) { + Regiment r = company.getRegiment(); + ItemStack i = r.getIcon().clone(); + ItemMeta meta = i.getItemMeta(); + meta.setDisplayName(company.getName()); + List lore = new ArrayList(); + lore.add("§7Soldiers: §e"+company.getFilledSlots()+" §7("+company.getSlots()+" slots)"); + lore.add(StringFormatter.formatHex("#877e7cOnly filled slots count toward offense")); + lore.add(" "); + lore.add(StringFormatter.formatHex("#a89977The main guild's mercenary company.")); + lore.add(StringFormatter.formatHex("#a89977Slots and upkeep are managed by the company.")); + meta.setLore(lore); + i.setItemMeta(meta); + return i; + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. + @SuppressWarnings("deprecation") public ItemStack createRegimentIncreaseButton(Faction f, Regiment r) { ItemAPI api = TLibs.getItemAPI(); ExpandResult expand = f.getMilitary().canExpand(r); diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryView.java b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryView.java index 20ddacc8..28b98f0c 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryView.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/inventory/MilitaryView.java @@ -15,6 +15,7 @@ import net.tfminecraft.simplefactions.managers.FactionManager; import net.tfminecraft.simplefactions.managers.InventoryManager; import net.tfminecraft.simplefactions.managers.holder.SFInventoryHolder; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany; import net.tfminecraft.simplefactions.objects.Faction; import net.tfminecraft.simplefactions.enums.SFGUI; import net.tfminecraft.simplefactions.keys.Keys; @@ -55,7 +56,13 @@ public void militaryView(Inventory i, Player player, Faction f, boolean open) { } } } - for (int slot = 12 + m.getRegiments().size(); slot <= 44; slot++) { + int next = 12 + m.getRegiments().size(); + MercenaryCompany company = m.getHomeCompany(); + if(company != null) { + i.setItem(next, creator.createCompanyRegimentIcon(company)); + next++; + } + for (int slot = next; slot <= 44; slot++) { i.setItem(slot, new ItemStack(Material.AIR, 1)); } for(int x = 0; x<3; x++) { diff --git a/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompany.java b/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompany.java index c6a2d960..659a9457 100644 --- a/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompany.java +++ b/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompany.java @@ -363,6 +363,17 @@ public boolean enlist(String player) { return true; } + /** + * The founding slot goes to the guild leader, so a new company opens with its + * first soldier. A leader already serving in another company keeps that post + * and the slot stays open. + */ + public void enlistLeader() { + String leader = getLeader(); + if (leader == null || MercenaryCompanies.findByMember(leader) != null) return; + enlist(leader); + } + /** Frees the slot, which in turn freezes expansion until it is filled again. */ public boolean kick(String player) { for (int i = 0; i < enlisted.size(); i++) { @@ -522,6 +533,7 @@ public void tick() { formationRemaining--; if (formationRemaining == 0 && regiment != null) { regiment.setCurrentSlots(1); + enlistLeader(); chime(SFGUI.COMPANY_VIEW); } return; diff --git a/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyService.java b/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyService.java index 5c67e022..9ec55903 100644 --- a/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyService.java +++ b/src/main/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyService.java @@ -74,6 +74,7 @@ public static MercenaryResult requestFormation(Guild guild, String actor, String MercenaryCompany company = new MercenaryCompany( guild, displayName(name), regiment, Cache.mercenaryFormationSeconds); guild.setCompany(company); + if (company.isFormed()) company.enlistLeader(); return MercenaryResult.ok("Founding " + company.getName() + "§a for " + money(cost) + " from the guild bank. It will be ready in " + hours(Cache.mercenaryFormationSeconds) + "."); diff --git a/src/test/java/net/tfminecraft/simplefactions/mercenary/company/CompanyGuiTest.java b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/CompanyGuiTest.java index d6b22797..1c55687d 100644 --- a/src/test/java/net/tfminecraft/simplefactions/mercenary/company/CompanyGuiTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/CompanyGuiTest.java @@ -168,6 +168,7 @@ void maxedUpgradeSaysSoAndKeepsTheWarning() { @Test void emptySlotLoreExplainsWhyExpansionIsFrozen() { MercenaryCompany company = formedCompany(); + company.kick(fixture.leader()); List empty = creator.buildSlotLore(null); assertTrue(empty.stream().anyMatch(line -> line.contains("Empty"))); diff --git a/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyFormationTest.java b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyFormationTest.java index 551e4214..d3b2fa04 100644 --- a/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyFormationTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryCompanyFormationTest.java @@ -56,6 +56,43 @@ void formationDebitsOnceAndOpensWithOneSlot() { assertTrue(company.isFormed()); assertEquals(1, company.getSlots()); assertEquals(400.0, fixture.balance()); + assertEquals(java.util.List.of("Ivar"), company.getEnlisted()); + assertEquals(1, company.getFilledSlots()); + assertFalse(company.hasFreeSlot()); + } + + @Test + void instantFormationEnlistsTheLeader() { + Cache.mercenaryFormationSeconds = 0; + CompanyFixture fixture = new CompanyFixture(500.0); + + MercenaryCompanyService.requestFormation(fixture.guild, "Ivar", "Hired Blades"); + + MercenaryCompany company = fixture.company(); + assertTrue(company.isFormed()); + assertTrue(company.isEnlisted("Ivar")); + assertEquals(1, company.getFilledSlots()); + } + + @Test + void leaderServingElsewhereLeavesTheFoundingSlotOpen() { + CompanyFixture other = new CompanyFixture(500.0); + MercenaryCompanyService.requestFormation(other.guild, "Ivar", "Other Blades"); + other.company().tick(); + other.company().tick(); + CompanyFixture fixture = new CompanyFixture(500.0); + CompanyFixture.registerGlobally(other.guild, fixture.guild); + try { + MercenaryCompanyService.requestFormation(fixture.guild, "Ivar", "Hired Blades"); + fixture.company().tick(); + fixture.company().tick(); + + assertTrue(fixture.company().isFormed()); + assertTrue(fixture.company().getEnlisted().isEmpty()); + assertTrue(fixture.company().hasFreeSlot()); + } finally { + CompanyFixture.clearGlobalGuilds(); + } } @Test diff --git a/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java new file mode 100644 index 00000000..44966e27 --- /dev/null +++ b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java @@ -0,0 +1,89 @@ +package net.tfminecraft.simplefactions.mercenary.company; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.simplefactions.Cache; +import net.tfminecraft.simplefactions.army.Military; +import net.tfminecraft.simplefactions.loaders.RegimentLoader; +import net.tfminecraft.simplefactions.objects.Faction; +import net.tfminecraft.simplefactions.objects.handler.GuildHandler; + +class MercenaryHomeManpowerTest { + private int oldSeconds; + private Faction faction; + private GuildHandler handler; + private CompanyFixture fixture; + + @BeforeEach + void setUp() { + oldSeconds = Cache.mercenaryFormationSeconds; + Cache.mercenaryFormationSeconds = 0; + RegimentLoader.oList.clear(); + RegimentLoader.oList.add(CompanyFixture.regularPrototype("militia", 6, 2.0)); + RegimentLoader.oList.add(CompanyFixture.prototype()); + + fixture = new CompanyFixture(500.0); + faction = mock(Faction.class); + handler = mock(GuildHandler.class); + when(faction.getId()).thenReturn("hired_blades"); + when(faction.getGuildHandler()).thenReturn(handler); + } + + @AfterEach + void tearDown() { + Cache.mercenaryFormationSeconds = oldSeconds; + CompanyFixture.clearRegiments(); + } + + private MercenaryCompany found() { + MercenaryCompanyService.requestFormation(fixture.guild, "Ivar", "Hired Blades"); + return fixture.company(); + } + + @Test + void mainGuildCompanyAddsFilledSlotsOnly() { + when(handler.getGuild("hired_blades")).thenReturn(fixture.guild); + MercenaryCompany company = found(); + company.adminAdjustSlots(2); + Military military = new Military(faction); + + assertSame(company, military.getHomeCompany()); + assertEquals(1, military.getMercenaryManpower()); + assertEquals(7, military.getManpower(true)); + assertEquals(7, military.getManpower(false)); + + company.enlist("Sigrun"); + + assertEquals(8, military.getManpower(true)); + assertEquals(6, military.getTotalSlots()); + } + + @Test + void companyOfAnotherGuildDoesNotCount() { + when(handler.getGuild("hired_blades")).thenReturn(mock(net.tfminecraft.simplefactions.guild.Guild.class)); + found(); + Military military = new Military(faction); + + assertNull(military.getHomeCompany()); + assertEquals(6, military.getManpower(true)); + } + + @Test + void foundingCompanyDoesNotCount() { + Cache.mercenaryFormationSeconds = 10; + when(handler.getGuild("hired_blades")).thenReturn(fixture.guild); + found(); + Military military = new Military(faction); + + assertNull(military.getHomeCompany()); + assertEquals(6, military.getManpower(true)); + } +} diff --git a/src/test/java/net/tfminecraft/simplefactions/mercenary/contract/ContractFixture.java b/src/test/java/net/tfminecraft/simplefactions/mercenary/contract/ContractFixture.java index 60865238..00220fc2 100644 --- a/src/test/java/net/tfminecraft/simplefactions/mercenary/contract/ContractFixture.java +++ b/src/test/java/net/tfminecraft/simplefactions/mercenary/contract/ContractFixture.java @@ -42,6 +42,8 @@ public static ContractFixture formed(int slots) { for (int i = 0; i < Cache.mercenaryFormationSeconds; i++) { company.tick(); } + // Formation enlists the leader; release them so each test names its own roster. + company.kick(host.leader()); // Formation grants one slot; the rest are filled and grown so expansion is legal. while (company.getSlots() < slots) { for (int i = 0; i < company.getSlots(); i++) { From 1d8fa6c3b4f7397380edd3a94d1631162336b87c Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:56:00 +0000 Subject: [PATCH 2/2] Skip a home company that loaded without its regiment Co-Authored-By: Claude Opus 5.5 (1M context) --- .../net/tfminecraft/simplefactions/army/Military.java | 7 ++++--- .../mercenary/company/MercenaryHomeManpowerTest.java | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/tfminecraft/simplefactions/army/Military.java b/src/main/java/net/tfminecraft/simplefactions/army/Military.java index 380ea999..b1fa9d45 100644 --- a/src/main/java/net/tfminecraft/simplefactions/army/Military.java +++ b/src/main/java/net/tfminecraft/simplefactions/army/Military.java @@ -144,8 +144,9 @@ public ExpandResult adminAdjustSlots(String regimentId, int delta) { } /** - * The main guild's company, when it has finished founding. Only this company - * fights for the faction; companies of other guilds are hired by contract. + * The main guild's company, once it has finished founding and has a regiment. + * Only this company fights for the faction; companies of other guilds are + * hired by contract. */ public MercenaryCompany getHomeCompany() { if(f == null) return null; @@ -154,7 +155,7 @@ public MercenaryCompany getHomeCompany() { Guild main = handler.getGuild(f.getId()); if(main == null) return null; MercenaryCompany company = main.getCompany(); - return company != null && company.isFormed() ? company : null; + return company != null && company.isFormed() && company.getRegiment() != null ? company : null; } /** Filled slots of the home company. Empty slots add nothing. */ diff --git a/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java index 44966e27..84ed9cee 100644 --- a/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/mercenary/company/MercenaryHomeManpowerTest.java @@ -86,4 +86,15 @@ void foundingCompanyDoesNotCount() { assertNull(military.getHomeCompany()); assertEquals(6, military.getManpower(true)); } + + @Test + void companyWithoutARegimentDoesNotCount() { + MercenaryCompany company = new MercenaryCompany(fixture.guild, "Hired Blades", null, 0); + fixture.guild.setCompany(company); + when(handler.getGuild("hired_blades")).thenReturn(fixture.guild); + Military military = new Military(faction); + + assertNull(military.getHomeCompany()); + assertEquals(6, military.getManpower(true)); + } }