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 @@ -46,6 +46,7 @@
import net.tfminecraft.simplefactions.government.movement.Action;
import net.tfminecraft.simplefactions.government.movement.Movement;
import net.tfminecraft.simplefactions.government.movement.PoliticalAction;
import net.tfminecraft.simplefactions.government.movement.UsurpCause;
import net.tfminecraft.simplefactions.government.movement.Pool;
import net.tfminecraft.simplefactions.government.movement.cause.Cause;
import net.tfminecraft.simplefactions.government.proposal.Proposal;
Expand Down Expand Up @@ -438,6 +439,7 @@ public boolean canProposePolitical(Player p, Action action) {
break;
case VASSAL_LEADER:
if(!PoliticalActionLoader.getByAction(action).allowFactions()) return false;
if(action == Action.USURP && !UsurpCause.canUsurp(FactionManager.getByLeader(name), f)) return false;
default:
break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public enum Action {
CHANGE_LEADER("#c5e0e3Change Leader"),
NATIONHOOD("#c5e0e3Demand Nationhood"),
INDEPENDENCE("#c5e0e3Declare Independence"),
USURP("#c5e0e3Usurp the Title"),
SNAP_ELECTIONS("#c5e0e3Call Snap Elections"),
DISSOLVE("#c5e0e3Dissolve Faction"),
WHITE_PEACE("#c5e0e3White Peace"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ public PoliticalAction(Action action) {
this.action = action;
this.icon = "v.paper";
this.name = action.getDisplay();
this.description = new ArrayList<>();
this.pools = Arrays.asList("citizens", "guilds", "factions");
this.description = action == Action.USURP
? new ArrayList<>(List.of(StringFormatter.formatHex("#c4c0a1The cause leader's faction takes the highest title"),
StringFormatter.formatHex("#c4c0a1and becomes the overlord of this faction.")))
: new ArrayList<>();
// Only a vassal can take its overlord's title; everything else is open to all pools.
this.pools = action == Action.USURP
? List.of("vassals")
: Arrays.asList("citizens", "guilds", "factions");
}

public Action getAction() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.tfminecraft.simplefactions.government.movement;

import net.tfminecraft.simplefactions.managers.FactionManager;
import net.tfminecraft.simplefactions.managers.RelationManager;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.government.movement.cause.Cause;
import net.tfminecraft.simplefactions.war.declare.WarGoalValidator;

/**
* A vassal takes its overlord's highest title through a movement in the overlord's
* government, not a war declared from outside. The cause leader's faction usurps.
*/
public final class UsurpCause {
private UsurpCause() {}

/**
* True when {@code vassal} is a direct subject of {@code overlord} and may take its title.
* The usurper must be allowed vassals, since the old overlord becomes its subject.
*/
public static boolean canUsurp(Faction vassal, Faction overlord) {
if (vassal == null || overlord == null || overlord.getHighestTitle() == null || !vassal.canHaveVassals()) {
return false;
}
if (!RelationManager.isOverlord(vassal, overlord)) {
return false;
}
return WarGoalValidator.canUsurpByRank(vassal.getTier().getTier(), overlord.getTier().getTier());
}

/** The faction that takes the title: the cause leader's, else the first eligible faction in the cause. */
public static Faction usurper(Cause cause, Faction overlord) {
if (cause == null) {
return null;
}
Faction led = cause.hasLeader() ? FactionManager.getByMember(cause.getLeader()) : null;
if (canUsurp(led, overlord)) {
return led;
}
for (Faction faction : cause.getPool().getFactions()) {
if (canUsurp(faction, overlord)) {
return faction;
}
}
return null;
}

public static void apply(Cause cause, Faction overlord) {
Faction usurper = usurper(cause, overlord);
if (usurper != null) {
FactionManager.usurp(null, usurper, overlord);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ public ItemStack getAsBook(Player p) {
case INDEPENDENCE:
first.add(StringFormatter.formatHex("#d4c9aeMembers of the cause are granted independence"));
break;
case USURP:
first.add(StringFormatter.formatHex("#d4c9aeThe cause leader's faction takes this faction's highest title"));
break;
case NATIONHOOD:
first.add(StringFormatter.formatHex("#d4c9aeMembers of the cause are elevated to nationhood"));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public void load(File configFile) {
PoliticalAction action = new PoliticalAction(key, config.getConfigurationSection(key));
loaded.put(action.getAction(), action);
}
// Usurp is newer than most servers' copy of this file; it is vassal-only by default.
loaded.putIfAbsent(Action.USURP, new PoliticalAction(Action.USURP));
} catch (IOException | InvalidConfigurationException | RuntimeException e) {
throw new IllegalStateException("Cannot load definitions from " + configFile, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@ public void confirmWarDeclareView(Player player, WarDeclareRequest request) {
pendingWarDeclares.put(player, request);
confirming.put(player, request.getAttacker());
Inventory i = SimpleFactions.plugin.getServer().createInventory(null, 27, "§7Confirm Action");
i.setItem(13, declareWarView.creator.createConfirmSummaryItem(request));
i.setItem(13, declareWarView.creator.createConfirmSummaryItem(request, DeclareWarView.needsCode(player, request)));
i.setItem(11, createButton("confirm", "war_declare", request.getDefender().getId()));
i.setItem(15, createButton("cancel", "war_declare", request.getDefender().getId()));
player.openInventory(i);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import net.tfminecraft.simplefactions.war.declare.DeJureAnnexEligibility.KeptSettlement;
import net.tfminecraft.simplefactions.war.declare.OpenMarketEligibility;
import net.tfminecraft.simplefactions.war.declare.PillageEligibility;
import net.tfminecraft.simplefactions.war.declare.WarGoalPreview;
import net.tfminecraft.simplefactions.war.declare.WarDeclareRequest;
import net.tfminecraft.simplefactions.war.enums.WarGoalType;
import net.tfminecraft.simplefactions.laws.Law;
Expand All @@ -34,9 +35,10 @@

public class DeclareWarCreator {

public ItemStack createTributaryGoalItem() {
return createGoalItem(WarGoalType.TRIBUTARY, "§lTributary", List.of(
"§7Make the defender your tributary."));
public ItemStack createTributaryGoalItem(Faction attacker, Faction defender) {
List<String> lore = new ArrayList<>(List.of("§7Make the defender your tributary."));
lore.addAll(WarGoalPreview.subjectEffects(RelationLoader.getType("tributary"), attacker, defender));
return createGoalItem(WarGoalType.TRIBUTARY, "§lTributary", lore);
}

public ItemStack createSubjugateGoalItem() {
Expand All @@ -45,16 +47,24 @@ public ItemStack createSubjugateGoalItem() {
"§7your subject."));
}

public ItemStack createUsurpGoalItem() {
return createGoalItem(WarGoalType.USURP, "§lUsurp", List.of(
public ItemStack createUsurpGoalItem(Faction defender) {
List<String> lore = new ArrayList<>(List.of(
"§7Take the defender's primary title",
"§7and become their liege."));
lore.addAll(WarGoalPreview.usurp(defender));
return createGoalItem(WarGoalType.USURP, "§lUsurp", lore);
}

public ItemStack createOpenMarketGoalItem() {
return createGoalItem(WarGoalType.OPEN_MARKET, "§lOpen Market", List.of(
public ItemStack createOpenMarketGoalItem(Faction defender) {
List<String> lore = new ArrayList<>(List.of(
"§7Force the defender to adopt",
"§7the configured free-trade law."));
OpenMarketEligibility.ResolvedLaw resolved =
OpenMarketEligibility.resolve(defender, Cache.openMarketApplyDefenderLaw);
if (resolved != null && resolved.law() != null && resolved.law().getName() != null) {
lore.add("§7Law: §f" + resolved.law().getName());
}
return createGoalItem(WarGoalType.OPEN_MARKET, "§lOpen Market", lore);
}

public ItemStack createChangeGovernmentGoalItem() {
Expand All @@ -63,10 +73,32 @@ public ItemStack createChangeGovernmentGoalItem() {
"§7government and leadership combo."));
}

public ItemStack createPillageGoalItem() {
return createGoalItem(WarGoalType.PILLAGE, "§lPillage", List.of(
/** {@code bestLoot}: the richest eligible settlement's line, or null when none is in range. */
public ItemStack createPillageGoalItem(String bestLoot) {
List<String> lore = new ArrayList<>(List.of(
"§7Raid a nearby settlement for",
"§7trade income and loot."));
lore.add(bestLoot != null ? bestLoot : "§cNo settlement in range to pillage.");
return createGoalItem(WarGoalType.PILLAGE, "§lPillage", lore);
}

/** Shown above the goals: what every war wins or costs, and when the staff code is asked for. */
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
public ItemStack createOverviewItem(Faction attacker, Faction defender, boolean codeRequired) {
ItemStack item = new ItemStack(Material.GOLD_NUGGET);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName("§6War with " + defender.getName());
List<String> lore = new ArrayList<>(WarGoalPreview.reparations(attacker, defender));
lore.add(" ");
lore.add("§7Hover over a goal to see what it wins.");
if (codeRequired) {
lore.add("§7You only need a staff war code");
lore.add("§7when you confirm the declaration.");
}
meta.setLore(lore);
item.setItemMeta(meta);
return item;
}

public ItemStack createDeJureGoalItem() {
Expand Down Expand Up @@ -100,13 +132,14 @@ private ItemStack createGoalItem(WarGoalType goal, String name, List<String> des

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
public ItemStack createRelationTypeItem(RelationType type, Faction attacker) {
public ItemStack createRelationTypeItem(RelationType type, Faction attacker, Faction defender) {
ItemStack item = IconGetter.getIcon("war");
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(type.getName());
List<String> lore = new ArrayList<>();
lore.add("§7The defender becomes this");
lore.add("§7kind of subject.");
lore.addAll(WarGoalPreview.subjectEffects(type, attacker, defender));
if (attacker != null && RelationManager.atLimit(attacker, type)) {
lore.add("§cAt limit");
}
Expand Down Expand Up @@ -174,7 +207,8 @@ public ItemStack createSettlementItem(PillageEligibility.PillageSettlementOption
ItemStack item = new ItemStack(Material.PAPER, 1);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName("§7" + settlement.getName());
List<String> lore = new ArrayList<>();
List<String> lore = new ArrayList<>(WarGoalPreview.pillage(settlement));
lore.add(" ");
if (option.eligible()) {
lore.add("§eClick to declare");
} else if (option.blockReason() != null) {
Expand Down Expand Up @@ -263,7 +297,7 @@ public ItemStack createGovernmentConfirmItem() {

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
public ItemStack createConfirmSummaryItem(WarDeclareRequest request) {
public ItemStack createConfirmSummaryItem(WarDeclareRequest request, boolean asksForCode) {
ItemStack item = IconGetter.getIcon("war");
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(StringFormatter.formatHex("#d42300§lDeclare War?"));
Expand Down Expand Up @@ -303,6 +337,7 @@ public ItemStack createConfirmSummaryItem(WarDeclareRequest request) {
Settlement settlement = PillageEligibility.findSettlement(request.getTargetSettlementId(), request.getDefender());
if (settlement != null && settlement.getName() != null) {
lore.add(StringFormatter.formatHex("#a89977Settlement: "+settlement.getName()));
lore.addAll(WarGoalPreview.pillage(settlement));
}
}
if (request.getGoal() == WarGoalType.USURP) {
Expand All @@ -317,6 +352,11 @@ public ItemStack createConfirmSummaryItem(WarDeclareRequest request) {
lore.addAll(coalitionWarnings);
}
lore.add(" ");
lore.addAll(WarGoalPreview.reparations(request.getAttacker(), request.getDefender()));
lore.add(" ");
if (asksForCode) {
lore.add("§eConfirming asks for your staff war code.");
}
lore.add(StringFormatter.formatHex("#c74c3fThis cannot be undone easily."));
meta.setLore(lore);
item.setItemMeta(meta);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
import net.tfminecraft.simplefactions.war.core.WarDemand;
import net.tfminecraft.simplefactions.war.enums.WarGoalType;
import net.tfminecraft.simplefactions.war.declare.ChangeGovernmentEligibility;
import net.tfminecraft.simplefactions.war.declare.DeclareCodePrompt;
import net.tfminecraft.simplefactions.war.declare.DeJureAnnexEligibility.DeJureTitleOption;
import net.tfminecraft.simplefactions.war.declare.PillageEligibility;
import net.tfminecraft.simplefactions.war.declare.WarDeclareCodeService;
import net.tfminecraft.simplefactions.war.declare.WarDeclareRequest;
import net.tfminecraft.simplefactions.war.declare.WarDemandRules;
import net.tfminecraft.simplefactions.war.declare.WarGoalPreview;
import net.tfminecraft.simplefactions.war.declare.WarGoalValidator;
import net.tfminecraft.simplefactions.war.declare.WarValidationResult;
import net.tfminecraft.simplefactions.enums.SFGUI;
Expand Down Expand Up @@ -75,21 +77,26 @@ public void openGoalPicker(
inventory.clear();
int slot = 10;
boolean withinRealm = withinRealm(demand);
if (demand == null) {
inventory.setItem(4, creator.createOverviewItem(
attacker, defender, WarDeclareCodeService.isRequired(player)));
}
// A ruler punishing their own vassal cannot set them free as a tributary.
if (!withinRealm) {
inventory.setItem(slot++, creator.createTributaryGoalItem());
inventory.setItem(slot++, creator.createTributaryGoalItem(attacker, defender));
}
if (attacker.canHaveVassals()) {
inventory.setItem(slot++, creator.createSubjugateGoalItem());
}
inventory.setItem(slot++, creator.createOpenMarketGoalItem());
inventory.setItem(slot++, creator.createOpenMarketGoalItem(defender));
inventory.setItem(slot++, creator.createChangeGovernmentGoalItem());
// Pillage is fought over its own settlement, so it can only be a war's first goal.
if (demand == null) {
inventory.setItem(slot++, creator.createPillageGoalItem());
inventory.setItem(slot++, creator.createPillageGoalItem(
WarGoalPreview.bestPillage(PillageEligibility.options(attacker, defender))));
}
if (!withinRealm && WarDeclareHelper.canDeclareUsurp(attacker, defender)) {
inventory.setItem(slot++, creator.createUsurpGoalItem());
inventory.setItem(slot++, creator.createUsurpGoalItem(defender));
}
if (!titleOptions(attacker, defender, demand).isEmpty()) {
inventory.setItem(slot++, creator.createDeJureGoalItem());
Expand Down Expand Up @@ -123,7 +130,7 @@ public void openRelationTypePicker(
int slot = 0;
for (RelationType type : subjectTypes(demand)) {
if (slot >= 26) break;
inventory.setItem(slot++, creator.createRelationTypeItem(type, attacker));
inventory.setItem(slot++, creator.createRelationTypeItem(type, attacker, defender));
}
inventory.setItem(26, inv.createBackButton(SFGUI.WAR_DECLARE_RELATION_TYPE));
relabelForDemand(inventory, demand);
Expand Down Expand Up @@ -408,9 +415,21 @@ public void handleConfirm(Player player, WarDeclareRequest request, boolean conf
returnToPreviousPicker(player, request);
return;
}
if (needsCode(player, request)) {
// Staff bypass the code by permission, which is what makes it safe for an
// unreachable ProvinceSystem to fail closed here.
DeclareCodePrompt.begin(player, request);
return;
}
executeDeclare(player, request);
}

/** True when the leader still has to enter a staff code that covers this pairing. */
public static boolean needsCode(Player player, WarDeclareRequest request) {
return WarDeclareCodeService.isRequired(player)
&& !WarDeclareCodeService.covers(WarDeclareCodeService.session(player), request);
}

/** Back out of a goal's sub-picker. A code session stays open: it covers the pairing, not a goal. */
private void backOutOfSubPicker(Player player, Faction attacker, Faction defender, WarDemandDraft demand) {
if (demand != null) {
Expand Down Expand Up @@ -522,17 +541,9 @@ private void returnToDemands(Player player, WarDemandDraft demand) {
}

private void executeDeclare(Player player, WarDeclareRequest request) {
// The war button gates the entry, but pendingWarDeclares is a separate map, so
// the session is re-checked here against the pairing actually confirmed.
WarDeclareCodeService.Session session = null;
if (WarDeclareCodeService.isRequired(player)) {
session = WarDeclareCodeService.session(player);
if (!WarDeclareCodeService.covers(session, request)) {
WarDeclareCodeService.clearSession(player);
player.sendMessage("§cYour war code does not cover this declaration.");
return;
}
}
// handleConfirm has already checked that this session covers the confirmed pairing.
WarDeclareCodeService.Session session =
WarDeclareCodeService.isRequired(player) ? WarDeclareCodeService.session(player) : null;
War war = WarManager.declareWar(
request.getAttacker(),
request.getDefender(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
import net.tfminecraft.simplefactions.managers.holder.SFInventoryHolder;
import net.tfminecraft.simplefactions.objects.Faction;
import net.tfminecraft.simplefactions.tiers.Tier;
import net.tfminecraft.simplefactions.war.declare.DeclareCodePrompt;
import net.tfminecraft.simplefactions.war.declare.WarDeclareCodeService;
import net.tfminecraft.simplefactions.enums.SFGUI;

public class RelationView {
Expand Down Expand Up @@ -302,13 +300,7 @@ public void click(InventoryClickEvent e, Inventory inventory, Player p) {
p.sendMessage("§cCannot declare war while none of the target faction members are online!");
return;
}
if (WarDeclareCodeService.isRequired(p)) {
// Staff bypass the code entirely by permission, which is what makes it
// safe for an unreachable ProvinceSystem to fail closed here.
p.playSound(p, Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f);
DeclareCodePrompt.begin(p, attacker, f);
return;
}
// Leaders browse the goals freely; the staff code is asked for at confirm.
p.playSound(p, Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f);
inv.openDeclareWarGoalPicker(p, attacker, f);
}
Expand Down
Loading
Loading