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 @@ -419,6 +419,7 @@ public boolean saveFactionChecked(Faction f) {
gd.repressed = g.isRepressed();
gd.favoured = g.isFavoured();
gd.dividendPercent = g.getDividendPercent();
gd.donation = g.getDonationAmount();
gd.dividendEligible = g.getDividendEligibleSnapshot();
gd.casinoProfit = g.getLedger().getCasinoProfit();
gd.vehicleFeeIncome = g.getLedger().getVehicleFeeIncome();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public class GuildData {
public List<StabilityModifierData> pillageHits = new ArrayList<>();

public Double dividendPercent;
/** Denars this guild sends to the realm each day, before the fee. */
public Double donation;
public List<String> dividendEligible = new ArrayList<>();

@SerializedName("casino profit")
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/net/tfminecraft/simplefactions/guild/Guild.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ public class Guild {
private boolean repressed = false;
private final List<StabilityModifier> pillageHits = new ArrayList<>();
private double dividendPercent = 0.0;
/** Denars sent to the realm each day. The fee is extra and is not stored here. */
private double donationAmount = 0.0;
private List<String> dividendEligible = new ArrayList<>();
private MercenaryCompany company;

Expand Down Expand Up @@ -235,6 +237,9 @@ public Guild(GuildData data, Faction host) {
if (data.dividendPercent != null) {
this.dividendPercent = clampDividendPercent(data.dividendPercent);
}
if (data.donation != null && !isBase()) {
this.donationAmount = clampDonation(data.donation);
}
if (data.dividendEligible != null) {
this.dividendEligible = new ArrayList<>(data.dividendEligible);
} else {
Expand Down Expand Up @@ -1029,6 +1034,23 @@ public double setDividendPercent(double percent) {
return dividendPercent;
}

public double getDonationAmount() {
return donationAmount;
}

/** {@code amount} is what the realm receives. Zero clears the pledge. Realm guilds cannot donate. */
public double setDonationAmount(double amount) {
donationAmount = isBase() ? 0.0 : clampDonation(amount);
return donationAmount;
}

private static double clampDonation(double amount) {
if (Double.isNaN(amount) || Double.isInfinite(amount) || amount <= 0) {
return 0.0;
}
return Formatter.formatDouble(amount);
}

public List<String> getDividendEligibleSnapshot() {
return new ArrayList<>(dividendEligible);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public enum Cashflow {
INTEREST("#63cf71Interest", false, true),
TRIBUTE_PAYMENTS("#ab8568Tribute Payments", false, false),
TRIBUTES("#ab8568Tribute Payments", false, true),
// Voluntary gift to the realm. Not gross income: the denars were already taxed where they were earned.
DONATION_PAYMENTS("#cfc97cDonations", false, false),
DONATIONS("#cfc97cDonations", false, false),
OVERLORD_TAX("#b55e94Overlord Taxes", false, false),
WAR_REPARATIONS("#8a433bWar Reparations", false, true),
WAR_REPARATIONS_PAYMENT("#8a433bWar Reparations", false, false),
Expand Down Expand Up @@ -47,7 +50,8 @@ public enum Cashflow {
INSTALLATIONS("#706964Installations", true, false),
VEHICLE_UPKEEP("#a6659fVehicle Upkeep", true, false),
MILITARY_UPKEEP("#a6659fMilitary Upkeep", true, false),
NODES("#8a7a5cNodes", true, false);
NODES("#8a7a5cNodes", true, false),
DONATION_FEE("#cfc97cDonation Fee", true, false);

private final String display;
private final boolean affectsInflation;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package net.tfminecraft.simplefactions.guild.income;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

import net.tfminecraft.simplefactions.guild.Guild;
import net.tfminecraft.simplefactions.utils.Formatter;

/**
* A guild's daily gift to its realm treasury. The amount is what the realm receives.
* The sender also pays a fee on top, so a 100d gift costs 110d and the extra 10d is destroyed.
* That fee stops a guild from moving its whole balance into the realm for free.
*/
public final class GuildDonation {

public static final double FEE_RATE = 0.10;

private GuildDonation() {}

/** Denars destroyed on top of {@code sent}. Zero when nothing is sent. */
public static double fee(double sent) {
if (sent <= 0 || Double.isNaN(sent) || Double.isInfinite(sent)) {
return 0.0;
}
return Formatter.formatDouble(sent * FEE_RATE);
}

/** Gift plus fee. This is what must be on hand. */
public static double cost(double sent) {
if (sent <= 0 || Double.isNaN(sent) || Double.isInfinite(sent)) {
return 0.0;
}
return Formatter.formatDouble(sent + fee(sent));
}

public static void notifyCancelled(Guild guild, double sent) {
if (guild == null || sent <= 0) {
return;
}
String leaderName = guild.getLeader();
if (leaderName == null || leaderName.isBlank() || Bukkit.getServer() == null) {
return;
}
Player leader = Bukkit.getPlayerExact(leaderName);
if (leader == null) {
return;
}
leader.sendMessage("§cDaily donation of §e" + Formatter.formatMoney(sent)
+ "d §cwas cancelled. The guild could not cover the day.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,23 @@ public double getIncome(Cashflow cashflow) {
case WAGE_PAYMENTS:
amount = -getAggregatedPendingWages();
break;
case DONATIONS:
if (!guild.isBase()) return 0;
if (f.getGuildHandler() == null || f.getGuildHandler().getGuilds() == null) break;
for (Guild other : f.getGuildHandler().getGuilds()) {
if (other == null || other.isBase() || other.getLedger() == null) continue;
if (other.getLedger().skipsMoneyMovement()) continue;
amount += Math.max(0, other.getDonationAmount());
}
break;
case DONATION_PAYMENTS:
if (guild.isBase()) return 0;
amount = -Math.max(0, guild.getDonationAmount());
break;
case DONATION_FEE:
if (guild.isBase()) return 0;
amount = -GuildDonation.fee(guild.getDonationAmount());
break;
case PENALTIES:
if(!guild.isBase()) return 0;
amount = -f.getPenalty();
Expand Down Expand Up @@ -484,6 +501,7 @@ public double getNetIncome() {
case INTEREST:
case MERCENARY_CONTRACT:
case REFUNDS:
case DONATIONS:
net += getIncome(cf);
break;

Expand All @@ -507,6 +525,8 @@ public double getNetIncome() {
case MERCENARY_PAYMENTS:
case REFUND_PAYMENTS:
case WAGE_PAYMENTS:
case DONATION_PAYMENTS:
case DONATION_FEE:
net += getIncome(cf); // already negative
break;

Expand Down Expand Up @@ -578,6 +598,8 @@ public double getDividendBase() {
case MERCENARY_PAYMENTS:
case REFUND_PAYMENTS:
case WAGE_PAYMENTS:
case DONATION_PAYMENTS:
case DONATION_FEE:
net += getIncome(cf);
break;
default:
Expand Down Expand Up @@ -896,6 +918,9 @@ private double loanBudget(DailyGuildTransfers buffer) {
}

public void populateDailyTransfers(DailyGuildTransfers buffer) {
// Drop the gift before anything else is committed. Other costs may then still fit,
// so a guild that was short only because of the donation stays solvent.
cancelDonationIfUnaffordable();
if(guild.isBankrupt() && guild.getLoanHandler() != null && guild.getLoanHandler().getLoansTaken() != null) {
for(Loan loan : guild.getLoanHandler().getLoansTaken()) {
if(loan == null || !loan.isAutoPay()) continue;
Expand Down Expand Up @@ -954,6 +979,37 @@ private void clearSettledContractBuckets() {
company.clearPendingWages();
}

/**
* True when today's settlement, including the donation and its fee, would finish below zero.
* Dividends and loan payments are left out: both stop at the money that remains, so they
* cannot be what makes the guild insolvent. Lines already in the bank, or already withdrawn
* earlier today, are left out too.
*/
private boolean donationOverdraws() {
double net = getNetIncome();
net -= getIncome(Cashflow.DIVIDEND_PAYOUT);
net -= getIncome(Cashflow.DIVIDEND_PAYMENT);
net -= getIncome(Cashflow.LOAN_PAYMENTS);
net -= getIncome(Cashflow.INTEREST_PAYMENTS);
net -= getIncome(Cashflow.GAMBLING);
net -= getIncome(Cashflow.VEHICLE_FEES);
net -= getIncome(Cashflow.INSTALLATIONS);
net -= getIncome(Cashflow.VEHICLE_UPKEEP);
return Formatter.formatDouble(wealthOf(guild) + net) < 0;
}

private void cancelDonationIfUnaffordable() {
if (guild.isBase() || guild.getDonationAmount() <= 0) {
return;
}
if (!skipsMoneyMovement() && !donationOverdraws()) {
return;
}
double sent = guild.getDonationAmount();
guild.setDonationAmount(0);
GuildDonation.notifyCancelled(guild, sent);
}

private void applySettlementFor(Cashflow cf, DailyGuildTransfers buffer) {
if(skipsMoneyMovement()) return; //bankrupt guilds dont pay or receive money, they need to get our of bankrupcy first
switch (cf) {
Expand Down Expand Up @@ -1134,6 +1190,26 @@ private void applySettlementFor(Cashflow cf, DailyGuildTransfers buffer) {
return;
}

case DONATION_PAYMENTS: {
if (guild.isBase()) return;
double amount = guild.getDonationAmount();
if (amount <= 0) return;
Faction faction = guild.getFaction();
if (faction == null) return;
Guild capital = faction.getOrCreateMainGuild();
if (capital == null || capital == guild) return;
buffer.add(guild, capital, amount);
return;
}

case DONATION_FEE: {
if (guild.isBase()) return;
double fee = GuildDonation.fee(guild.getDonationAmount());
if (fee <= 0) return;
buffer.addExternalDelta(guild, -fee);
return;
}

case WAGE_PAYMENTS: {
MercenaryCompany company = getFormedCompany();
if (company == null) return;
Expand Down Expand Up @@ -1161,6 +1237,7 @@ private void applySettlementFor(Cashflow cf, DailyGuildTransfers buffer) {
case WAR_REPARATIONS:
case MERCENARY_CONTRACT:
case REFUNDS:
case DONATIONS:
default:
return;
}
Expand Down
Loading
Loading