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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Players shape a world of provinces and titles, organise settlements and guilds,

A field battle with capture points ends when a side runs out of lives, retreats, or wins by control. Each second, the side holding more points than any other gains `per_second` × its share of the points, and holding every point multiplies that by `all_points_multiplier`. A point counts as held at 50% capture. The first side to `target` wins. The settings are under `battle.control` in `config.yml` (defaults: 1000, 0.5 and 4, so holding 2 of 3 points takes about 50 minutes and holding all of them about 8). Sieges keep their contest timer.

Campaign battles wear armies down through wounds. When a field battle or siege starts, every force on a side puts lives into the side's shared pool: each fighting faction's regiments allowed in that battle, each non-fighting subject's levy (its levy share of every regiment, militia included; all of its militia when the battle is on its own province), and each hired mercenary company's members on the roster. When it ends, the side's deaths become wounds: split between forces by the lives each brought, militia first inside a force, then law and paid slots in proportion. No force loses more than it brought. Law-granted slots are full again after every battle. Wounded paid slots cannot fight and cost no upkeep, and they are lost when the war ends. Mercenary companies never lose slots; their wounds heal by `mercenary_heal_percent` of the company every `mercenary_heal_hours`. A faction has one army for all its wars, and recruiting during a war reinforces it. The settings are under `war.battle_military` in `war.yml`.

Each war keeps a ledger of its battles, so staff can correct losses at any time before it ends. `/war admin losses <war>` lists the battles and what the war's end will take, and `/war admin losses <war> <battle#> attacker|defender set|add|remove <deaths> [reason]` corrects a battle's deaths. `/war admin army <faction>` shows each regiment's slots, wounds per war and what can fight. `/war admin wounds <war> <faction> <regiment> add|remove <lives> [reason]` and `/war admin wounds company <guild> [set <lives>]` change wounds directly. Before a campaign battle starts, `/battle lives <battle> <side> set|add|remove <lives>` sets the lives that side starts with, and `auto` goes back to what its forces give.

Use `/faction findvehicles personal` to locate your current character's vehicles.
Faction leaders can also use `/faction findvehicles pool` or
`/faction findvehicles <installation id>`. Tab completion suggests the available
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/tfminecraft/simplefactions/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public static boolean requireProvinces(org.bukkit.command.CommandSender sender)
public static int warBattleLivesPerRegiment;
public static int warBattleMinSideLives;
public static int warBattleDeathsPerRegimentLoss;
public static double warMercenaryHealPercent = 25;
public static double warMercenaryHealHours = 6;
public static BattleLootMode battleLootMode;
public static List<String> battleLootCommands = new ArrayList<>();
public static String battleLootItemPath;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.simplefactions.army;

import net.tfminecraft.simplefactions.war.wounds.WarWounds;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -57,7 +58,7 @@ public double getTotalUpkeep() {
public double getRawTotalUpkeep() {
double d = 0;
for(Regiment r : regiments) {
d += r.getTotalUpkeep();
d += r.getTotalUpkeep(WarWounds.woundedSlots(f, r));
}
return format.formatDouble(d);
}
Expand Down Expand Up @@ -246,7 +247,7 @@ public int getManpower(boolean offense) {
manpower += e.getAmount();
}
} else {
manpower += r.getCurrentSlots();
manpower += WarWounds.availableSlots(f, r);
}
}
return manpower;
Expand All @@ -258,7 +259,7 @@ public int getManpowerNoLevy(boolean offense) {
if(r.isEquipment()) continue;
if(offense && !r.isOffensive()) continue;
if(r.isLevy()) continue;
manpower += r.getCurrentSlots();
manpower += WarWounds.availableSlots(f, r);
}
return manpower;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,10 @@ public void sizeDecrease() {
currentSlots--;
}

public double getTotalUpkeep() {
double d = 0;
int paidSlots = currentSlots-freeSlots;
/** Upkeep for the paid slots that are not wounded; wounded slots cost nothing until they are lost. */
public double getTotalUpkeep(int woundedSlots) {
int paidSlots = currentSlots - freeSlots - Math.max(0, woundedSlots);
if(paidSlots < 1) return 0;
d += paidSlots*upkeep;
return format.formatDouble(d);
return format.formatDouble(paidSlots*upkeep);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class BattleSideData {
public int maxLives;
// Absent in saves from before the start was recorded.
public Integer startLives;
public List<net.tfminecraft.simplefactions.war.wounds.BattleForce> forces;
public Integer livesOverride;
public String lifeType;
public List<String> warbandIds = new ArrayList<>();
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public class MercenaryCompanyData {
public Map<String, Double> peacetimeWageOverrides = new LinkedHashMap<>();
/** Wages accrued but not yet settled, keyed by player name. */
public Map<String, Double> pendingWages = new LinkedHashMap<>();
/** Wounded lives from battles; absent when the company is unwounded. */
public Integer woundLives;
/** When the stored wounds started healing. */
public String woundsHealFrom;
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ public class WarData {
public String endReason;
public SideData attackers;
public SideData defenders;
public List<CommitmentData> commitments;
/** The war's battle ledger; absent in saves from before wounds. */
public net.tfminecraft.simplefactions.war.wounds.WarLedger.Data ledger;

public WarData() {
campaignProvinces = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.simplefactions.economy.settlement;

import net.tfminecraft.simplefactions.war.wounds.WarWounds;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -41,19 +42,23 @@ public static FactionUpkeepPlan calculate(Faction faction, double available) {
boolean bank = SettlementGuilds.capital(faction).getBank() != null;
double wealth = bank ? available : 0;
double factor = 1 + faction.getModifier(FactionModifiers.MILITARY_UPKEEP).getAmount() / 100;
double army = armyCost(regiments, factor);
double army = armyCost(faction, regiments, factor);
// Unpaid upkeep only disbands slots that cost something: wounded slots are free and stay
// until the war ends, so their wounds never move onto new recruits.
if (!bank && army > 0) {
for (Regiment regiment : regiments) {
if (!regiment.isLevy()) regiment.setCurrentSlots(Math.min(regiment.getCurrentSlots(), regiment.getFreeSlots()));
if (!regiment.isLevy()) regiment.setCurrentSlots(Math.min(regiment.getCurrentSlots(),
regiment.getFreeSlots() + WarWounds.woundedSlots(faction, regiment)));
}
army = 0;
}
int guard = 0;
while (army > 0 && wealth < army && guard++ < 100000) {
Regiment shrink = regiments.stream().filter(r -> !r.isLevy() && r.getCurrentSlots() > r.getFreeSlots()).findFirst().orElse(null);
Regiment shrink = regiments.stream().filter(r -> !r.isLevy()
&& r.getCurrentSlots() > r.getFreeSlots() + WarWounds.woundedSlots(faction, r)).findFirst().orElse(null);
if (shrink == null) break;
shrink.sizeDecrease();
army = armyCost(regiments, factor);
army = armyCost(faction, regiments, factor);
}
long charge = bank && army > 0 ? Money.cents(army) : 0;
wealth -= Money.denars(charge);
Expand Down Expand Up @@ -98,9 +103,9 @@ private static FactionUpkeepPlan paused(Faction faction) {
faction.getInstallationHandler().serializeConstruction(), 0, 0, 0);
}

private static double armyCost(List<Regiment> regiments, double factor) {
private static double armyCost(Faction faction, List<Regiment> regiments, double factor) {
double raw = 0;
for (Regiment regiment : regiments) raw += regiment.getTotalUpkeep();
for (Regiment regiment : regiments) raw += regiment.getTotalUpkeep(WarWounds.woundedSlots(faction, regiment));
return Formatter.formatDouble(Formatter.formatDouble(raw) * factor);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.simplefactions.espionage;

import net.tfminecraft.simplefactions.war.wounds.WarWounds;
import net.tfminecraft.simplefactions.identity.Members;

import java.time.LocalDate;
Expand Down Expand Up @@ -682,7 +683,7 @@ private static void ledgerAndForces(Map<String, Double> values, Faction faction,
values.put("Daily net income", main.getLedger().getNetIncome());
values.put("Professional army", (double) faction.getMilitary().getRegiments().stream()
.filter(regiment -> regiment.isProfessional() && !regiment.isEquipment())
.mapToInt(regiment -> regiment.getCurrentSlots()).sum());
.mapToInt(regiment -> WarWounds.availableSlots(faction, regiment)).sum());
values.put("Levies", (double) (faction.getMilitary().getManpower(false) - faction.getMilitary().getManpowerNoLevy(false)));
values.put("Mercenaries", (double) faction.getMilitary().getMercenaryManpower());
values.put("Army", values.get("Professional army") + values.get("Levies") + values.get("Mercenaries"));
Expand All @@ -706,7 +707,8 @@ private static void ledgerAndForces(Map<String, Double> values, Faction faction,
}
for (var regiment : faction.getMilitary().getRegiments()) values.put("Regiment:" + regiment.getId()
+ (regiment.isLevy() ? ":Levies" : ":Soldiers"), (double) (regiment.isLevy()
? regiment.getEntries().stream().mapToInt(net.tfminecraft.simplefactions.army.LevyEntry::getAmount).sum() : regiment.getCurrentSlots()));
? regiment.getEntries().stream().mapToInt(net.tfminecraft.simplefactions.army.LevyEntry::getAmount).sum()
: WarWounds.availableSlots(faction, regiment)));
for (var installation : faction.getInstallationHandler().getAll()) values.put("Installation:" + installation.getId() + ":Level", (double) installation.getLevel());
if (faction.getGovernment() != null) {
values.put("Stability", faction.getGovernment().getStability());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ public void loadWar(File warFile) {
Cache.warBattleMinSideLives = config.getInt("war.battle_military.min_side_lives", 1);
int deathsPerRegiment = config.getInt("war.battle_military.deaths_per_regiment_loss", 5);
Cache.warBattleDeathsPerRegimentLoss = deathsPerRegiment > 0 ? deathsPerRegiment : 5;
Cache.warMercenaryHealPercent = Math.max(0, config.getDouble("war.battle_military.mercenary_heal_percent", 25));
double healHours = config.getDouble("war.battle_military.mercenary_heal_hours", 6);
Cache.warMercenaryHealHours = healHours > 0 ? healHours : 6;
Cache.battleLootMode = BattleLootMode.fromJson(config.getString("war.battle_loot.mode", "ITEM"));
if (Cache.battleLootMode == null) {
Cache.battleLootMode = BattleLootMode.COMMAND;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.simplefactions.managers;

import net.tfminecraft.simplefactions.war.wounds.WarWounds;
import net.tfminecraft.simplefactions.identity.Members;

import net.tfminecraft.simplefactions.espionage.EspionageAccess;
Expand Down Expand Up @@ -1565,6 +1566,10 @@ public void clickButton(InventoryClickEvent e) {
if(f == null || !Members.is(p, f.getLeader())) return;
Regiment r = f.getMilitary().getRegiment(data);
if(r == null) return;
if(WarWounds.availablePaidSlots(f, r) <= 0 && WarWounds.woundedSlots(f, r) > 0) {
p.sendMessage("§cWounded slots cannot be disbanded. They are lost when the war ends.");
return;
}
r.sizeDecrease();
p.sendMessage("§cDecreased size of "+r.getName());
p.playSound(p, Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ public static void reset(Faction origin, Faction target, boolean hostile) {

public static boolean endVassalage(Faction origin, Faction target, boolean hostile) {
if(isOverlord(origin, target) || isOverlord(target, origin)) {
net.tfminecraft.simplefactions.war.commitment.WarCommitmentService.onVassalageEnded(origin, target);
reset(origin, target, hostile);
net.tfminecraft.simplefactions.espionage.EspionageService.forgetReports(origin, target);
MercenaryLoyaltyWatcher.onRelationChanged(origin, target);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.simplefactions.managers;

import net.tfminecraft.simplefactions.war.wounds.WarWounds;
import net.tfminecraft.simplefactions.identity.Members;

import java.time.Instant;
Expand All @@ -25,7 +26,6 @@
import net.tfminecraft.simplefactions.war.notice.WarNotices;
import net.tfminecraft.simplefactions.war.core.Side;
import net.tfminecraft.simplefactions.war.core.War;
import net.tfminecraft.simplefactions.war.core.WarCommitment;
import net.tfminecraft.simplefactions.war.core.WarDeclareHelper;
import net.tfminecraft.simplefactions.war.campaign.runtime.BattleSideMembers;
import net.tfminecraft.simplefactions.war.campaign.runtime.pick.BattleInstallationPickService;
Expand All @@ -34,7 +34,6 @@
import net.tfminecraft.simplefactions.war.campaign.WarCampaignService;
import net.tfminecraft.simplefactions.war.campaign.buildup.BuildupService;
import net.tfminecraft.simplefactions.war.campaign.runtime.CampaignClock;
import net.tfminecraft.simplefactions.war.commitment.WarCommitmentService;
import net.tfminecraft.simplefactions.war.enums.WarEndReason;
import net.tfminecraft.simplefactions.war.enums.WarGoalType;
import net.tfminecraft.simplefactions.war.enums.WarType;
Expand Down Expand Up @@ -161,7 +160,6 @@ public static War declareWar(
lastDeclareError = navy.getMessage();
return null;
}
WarCommitmentService.commitAllParticipants(war);
addWar(war);
logWarDeclared(war, attacker, defender);
CoalitionWars.announce(war);
Expand Down Expand Up @@ -259,7 +257,6 @@ public static War startCivilWar(
// Register the snapshot before refreshing, so the military recognizes the active rebellion.
wars.add(war);
refreshTempRebelSlots(war);
WarCommitmentService.commitAllParticipants(war);
announceAndPersist(war);
logWarDeclared(war, attacker, defender);
return war;
Expand Down Expand Up @@ -322,14 +319,6 @@ private static ProvinceManager resolveProvinceManager() {
return plugin.getProvinceManager();
}

public static List<WarCommitment> getCommitmentsForWar(int warId) {
return WarCommitmentService.getCommitmentsForWar(warId);
}

static void clearCommitments(int warId) {
WarCommitmentService.clearCommitments(warId);
}

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
public static War addWar(War w) {
wars.add(w);
Expand Down Expand Up @@ -452,6 +441,9 @@ public static void endWar(War w, WarEndReason reason) {
TruceService.onWarEnded(w, reason);
FactionManager.getMap().enqueueOccupationFromWar(w);
WartimeInstallationService.revert(w);
// Every wounded paid slot of this war is lost now, once, while civil war rebels still hold
// the regiments they fought with.
WarWounds.settle(w);
CivilWarUntangleService.restore(w, reason);
WarOutcomeService.apply(w, reason);
WarCombatTeardownService.teardownCombatForWar(w);
Expand All @@ -460,7 +452,6 @@ public static void endWar(War w, WarEndReason reason) {
WarHistoryEvents.ended(w, reason);
BattleInstallationPickService.clearForNewBattleDay(w);
CampaignRaidService.clearForNewBattleDay(w);
clearCommitments(w.getId());
for (War war : wars) {
if (war.getId() == w.getId()) {
wars.remove(war);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.simplefactions.managers.inventory;

import net.tfminecraft.simplefactions.war.wounds.WarWounds;
import net.tfminecraft.simplefactions.war.freeze.PreparationFreeze;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -85,11 +86,15 @@ public ItemStack createRegimentIcon(Faction f, Regiment r) {
} else {
lore.add("§7Current Slots: §e"+r.getCurrentSlots());
}
int wounded = WarWounds.woundedSlots(f, r);
if(wounded > 0) {
lore.add("§cWounded: "+wounded+" §7(cannot fight, lost when the war ends)");
}
if(r.sentToOverlord() > 0) {
lore.add(StringFormatter.formatHex("#877e7c("+r.sentToOverlord()+" sent as levies)"));
}
double mod = 1.0 + f.getModifier(FactionModifiers.MILITARY_UPKEEP).getAmount()/100.0;
lore.add("§7Current Upkeep: §e"+Formatter.formatMoney(r.getTotalUpkeep()*mod)+"d §7("+Formatter.formatMoney(r.getUpkeep()*mod)+"d per slot)");
lore.add("§7Current Upkeep: §e"+Formatter.formatMoney(r.getTotalUpkeep(wounded)*mod)+"d §7("+Formatter.formatMoney(r.getUpkeep()*mod)+"d per slot)");
} else {
int total = 0;
for(LevyEntry e : r.getEntries()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.simplefactions.mercenary.company;

import net.tfminecraft.simplefactions.war.wounds.CompanyWounds;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -62,6 +63,8 @@ public class MercenaryCompany {
* before the daily tick that pays it out.
*/
private final Map<String, Double> pendingWages = new LinkedHashMap<>();
/** Wounds from battles, shared by every contract; they heal over time. */
private CompanyWounds wounds = new CompanyWounds();

public MercenaryCompany(Guild guild, String name, Regiment regiment, int formationSeconds) {
this.guild = guild;
Expand Down Expand Up @@ -126,6 +129,10 @@ public MercenaryCompany(Guild guild, MercenaryCompanyData data, Regiment regimen
accrueWage(e.getKey(), e.getValue() == null ? 0 : e.getValue());
}
}
if (data.woundLives != null && data.woundLives > 0) {
wounds = new CompanyWounds(data.woundLives,
data.woundsHealFrom == null ? java.time.Instant.EPOCH : java.time.Instant.parse(data.woundsHealFrom));
}
if (data.slotQueue != null && regiment != null) {
for (String entry : data.slotQueue) {
if (entry == null) continue;
Expand Down Expand Up @@ -254,6 +261,15 @@ public int getFilledSlots() {
return Math.min(enlisted.size(), getSlots());
}

public CompanyWounds getWounds() {
return wounds;
}

/** Slots not wounded right now; only these bring lives to a battle. */
public int getHealthySlots(java.time.Instant now) {
return wounds.healthySlots(now, getSlots());
}

public boolean hasFreeSlot() {
return enlisted.size() < getSlots();
}
Expand Down Expand Up @@ -723,6 +739,10 @@ public MercenaryCompanyData serialize() {
data.activeWageOverrides = new LinkedHashMap<>(wages.getActiveOverrides());
data.peacetimeWageOverrides = new LinkedHashMap<>(wages.getPeacetimeOverrides());
data.pendingWages = new LinkedHashMap<>(pendingWages);
if (wounds.storedLives() > 0) {
data.woundLives = wounds.storedLives();
data.woundsHealFrom = wounds.healFrom().toString();
}
return data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ public static int coveringMembers(
return found;
}

/** How many of the company's enlisted players are on this battle side's roster, whatever the contracts. */
public static int membersOnRoster(MercenaryCompany company, BattleSide side) {
int found = 0;
for (String name : company.getEnlisted()) {
UUID id = uuids == null ? null : uuids.uuidOf(name);
if (id != null && isOnRoster(side, id)) {
found++;
}
}
return found;
}

private static boolean isOnRoster(BattleSide side, UUID id) {
for (Warband band : side.getBands()) {
if (band != null && band.hasMember(id)) return true;
Expand Down
Loading
Loading