4124 lines
97 KiB
ActionScript
4124 lines
97 KiB
ActionScript
#priority init 2000
|
|
import hooks;
|
|
import saving;
|
|
import icons;
|
|
import util.formatting;
|
|
|
|
from settings.map_lib import SystemDesc;
|
|
import const SystemDesc@ getClosestSystem(const vec3d& point, Empire& presence, bool trade = false) from "system_pathing";
|
|
|
|
// {{{ Influence cards
|
|
export InfluenceCardClass;
|
|
export InfluenceCardSide;
|
|
export InfluenceVariableMode;
|
|
export InfluenceCardType;
|
|
export InfluenceCard;
|
|
export InfluenceCardEffect;
|
|
export InfluenceSideEquals;
|
|
export StackInfluenceCard;
|
|
export InfluenceCardPlayEvent;
|
|
export Targets;
|
|
export INDETERMINATE;
|
|
|
|
export getInfluenceCardClassSprite;
|
|
export getInfluenceCardClassTooltip;
|
|
export getInfluenceCardRarityColor;
|
|
export getInfluenceCardTypeCount;
|
|
export getInfluenceCardType;
|
|
export getInfluenceDeckSize;
|
|
export getDistributedInfluenceCardType;
|
|
|
|
#section server-side
|
|
import Empire@ getSenateLeader() from "influence_global";
|
|
#section all
|
|
|
|
//{{{ Types
|
|
enum InfluenceCardClass {
|
|
ICC_Action,
|
|
ICC_Vote,
|
|
ICC_Effect,
|
|
ICC_Support,
|
|
ICC_Event,
|
|
ICC_Instant,
|
|
ICC_Misc,
|
|
|
|
ICC_COUNT
|
|
};
|
|
|
|
enum InfluenceCardRarity {
|
|
ICR_Basic,
|
|
ICR_Common,
|
|
ICR_Uncommon,
|
|
ICR_Rare,
|
|
ICR_Epic,
|
|
|
|
ICR_COUNT
|
|
};
|
|
|
|
enum InfluenceCardSide {
|
|
ICS_Support,
|
|
ICS_Oppose,
|
|
ICS_Both,
|
|
ICS_Neutral,
|
|
|
|
ICS_COUNT
|
|
};
|
|
|
|
enum InfluenceVariableMode {
|
|
IVM_None,
|
|
IVM_Property,
|
|
IVM_PurchaseCost,
|
|
};
|
|
|
|
bool InfluenceSideEquals(InfluenceCardSide spec, bool side) {
|
|
switch(spec) {
|
|
case ICS_Oppose: return side == false;
|
|
case ICS_Support: return side == true;
|
|
case ICS_Both: return false;
|
|
case ICS_Neutral: return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const int INDETERMINATE = INT_MIN;
|
|
|
|
tidy final class InfluenceCardType {
|
|
uint id;
|
|
string ident;
|
|
string name;
|
|
string description;
|
|
string dlc;
|
|
|
|
InfluenceCardClass cls = ICC_Misc;
|
|
bool leaderOnly = false;
|
|
Sprite icon;
|
|
Color color;
|
|
|
|
double frequency = -1;
|
|
InfluenceCardRarity rarity = ICR_Common;
|
|
bool collapseUses = true;
|
|
|
|
int basePurchaseCost = 0;
|
|
int qualityPurchaseCost = 0;
|
|
int placementPurchaseCost = 1;
|
|
int usesPurchaseCost = 0;
|
|
|
|
int sideTarget = -1;
|
|
InfluenceCardSide sideMode = ICS_Neutral;
|
|
|
|
int basePlayCost = 0;
|
|
int qualityPlayCost = 0;
|
|
|
|
int baseWeight = 0;
|
|
int qualityWeight = 0;
|
|
|
|
int minQuality = 1;
|
|
int maxQuality = 1;
|
|
bool canOverquality = true;
|
|
|
|
int minUses = 1;
|
|
int maxUses = 1;
|
|
|
|
uint points = 0;
|
|
|
|
Targets targets;
|
|
array<InfluenceCardEffect@> hooks;
|
|
array<Hook@> ai;
|
|
|
|
void init() {
|
|
for(uint i = 0, cnt = hooks.length; i < cnt; ++i) {
|
|
if(!hooks[i].instantiate())
|
|
error("Could not instantiate hook: "+addrstr(hooks[i])+" in "+ident);
|
|
hooks[i].initTargets(targets);
|
|
hooks[i].init(this);
|
|
}
|
|
for(uint i = 0, cnt = ai.length; i < cnt; ++i) {
|
|
if(!ai[i].instantiate())
|
|
error("Could not instantiate AI hook: "+addrstr(ai[i])+" in card "+ident);
|
|
ai[i].initTargets(targets);
|
|
}
|
|
}
|
|
|
|
//Create an empty card, with no random generation.
|
|
InfluenceCard@ create(int uses = 1, int quality = 0) const {
|
|
InfluenceCard card;
|
|
card.quality = quality == 0 ? minQuality : quality;
|
|
if(!canOverquality)
|
|
card.quality = clamp(card.quality, minQuality, maxQuality);
|
|
card.uses = uses;
|
|
card.set(this);
|
|
return card;
|
|
}
|
|
|
|
//Draw a random new influence card of this type,
|
|
//as generated by this card's generators.
|
|
InfluenceCard@ generate() const {
|
|
InfluenceCard card;
|
|
generate(card);
|
|
return card;
|
|
}
|
|
|
|
void generate(InfluenceCard@ card) const {
|
|
card.quality = minQuality + floor(sqr(randomd()) * double(maxQuality - minQuality + 1));
|
|
card.uses = minUses + floor(sqr(sqr(randomd())) * double(maxUses - minUses + 1));
|
|
card.set(this);
|
|
for(uint i = 0, cnt = hooks.length; i < cnt; ++i)
|
|
hooks[i].generate(card);
|
|
}
|
|
|
|
bool canGenerateOnStack() const {
|
|
for(uint i = 0, cnt = hooks.length; i < cnt; ++i) {
|
|
if(!hooks[i].canGenerateOnStack())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Cards
|
|
tidy class InfluenceCard : Serializable, Savable {
|
|
const InfluenceCardType@ type;
|
|
Targets targets;
|
|
|
|
int id = -1;
|
|
int uses = -1;
|
|
int quality = 0;
|
|
Empire@ owner;
|
|
array<any> data;
|
|
|
|
InfluenceCard() {
|
|
}
|
|
|
|
InfluenceCard(const InfluenceCardType@ Type) {
|
|
set(Type);
|
|
}
|
|
|
|
int opCmp(const InfluenceCard@ other) {
|
|
if(type.cls != other.type.cls)
|
|
return int(type.cls) - int(other.type.cls);
|
|
if(type.rarity != other.type.rarity)
|
|
return int(type.rarity) - int(other.type.rarity);
|
|
if(type !is other.type)
|
|
return int(type.id) - int(other.type.id);
|
|
if(other.quality != quality)
|
|
return other.quality - quality;
|
|
return id - other.id;
|
|
}
|
|
|
|
void set(const InfluenceCardType@ Type) {
|
|
@type = Type;
|
|
targets.set(type.targets);
|
|
data.length = type.hooks.length;
|
|
}
|
|
|
|
int get_extraQuality() const {
|
|
return quality - type.minQuality;
|
|
}
|
|
|
|
uint get_points() const {
|
|
uint pts = type.points;
|
|
pts += 10 * type.basePurchaseCost;
|
|
pts += 10 * type.qualityPurchaseCost * (quality - type.minQuality);
|
|
return pts;
|
|
}
|
|
|
|
void resetData() {
|
|
data.length = 0;
|
|
data.length = type.hooks.length;
|
|
}
|
|
|
|
bool isActiveEffect(const InfluenceVote@ vote = null) const {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(type.hooks[i].isActiveEffect(this, null, vote))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//Networking
|
|
InfluenceCard(Message& msg) {
|
|
read(msg);
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg.writeLimited(type.id,getInfluenceCardTypeCount()-1);
|
|
|
|
targets.writeData(msg, type.targets);
|
|
msg << id;
|
|
msg << owner;
|
|
msg.writeSignedSmall(uses);
|
|
msg.writeSignedSmall(quality);
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
@type = getInfluenceCardType(msg.readLimited(getInfluenceCardTypeCount()-1));
|
|
|
|
targets.readData(msg, type.targets);
|
|
data.length = type.hooks.length;
|
|
msg >> id;
|
|
msg >> owner;
|
|
uses = msg.readSignedSmall();
|
|
quality = msg.readSignedSmall();
|
|
}
|
|
|
|
//Saving
|
|
InfluenceCard(SaveFile& file) {
|
|
load(file);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file.writeIdentifier(SI_InfluenceCard, type.id);
|
|
targets.saveData(file, type.targets);
|
|
file << id;
|
|
file << uses;
|
|
file << quality;
|
|
file << owner;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].save(this, file);
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
uint typeId = file.readIdentifier(SI_InfluenceCard);
|
|
@type = getInfluenceCardType(typeId);
|
|
data.length = type.hooks.length;
|
|
targets.loadData(file, type.targets);
|
|
file >> id;
|
|
file >> uses;
|
|
file >> quality;
|
|
file >> owner;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].load(this, file);
|
|
}
|
|
|
|
//Formatting
|
|
string formatTitle(bool pretty = true) const {
|
|
return targets.format(type.name, pretty);
|
|
}
|
|
|
|
string formatDescription(bool pretty = true) const {
|
|
return targets.format(type.description, pretty);
|
|
}
|
|
|
|
string formatBlurb(int uses = 0) const {
|
|
string title = formatTitle();
|
|
if(uses == 0)
|
|
uses = this.uses;
|
|
if(uses < 0)
|
|
title += " ("+locale::USES_UNLIMITED+")";
|
|
else if(uses > 1)
|
|
title += " ("+uses+"x)";
|
|
int qual = extraQuality;
|
|
if(qual > 0) {
|
|
title += " ";
|
|
for(int i = 0; i < qual; ++i)
|
|
title += "*";
|
|
}
|
|
title = format("[color=$2]$1[/color]", title, toString(type.color));
|
|
return title;
|
|
}
|
|
|
|
string formatTooltip(Empire@ purchaseEmpire = null, bool showUses = true) const {
|
|
string tt;
|
|
tt = formatTitle();
|
|
if(extraQuality != 0) {
|
|
tt += " ";
|
|
for(int i = 0, cnt = extraQuality; i < cnt; ++i)
|
|
tt += "*";
|
|
}
|
|
if(uses > 1 && showUses)
|
|
tt += format(" ($1x)", toString(uses));
|
|
tt = format("[b][font=Medium][color=$2]$1[/color][/font][/b]\n", tt, toString(type.color));
|
|
tt += formatDescription();
|
|
tt += "\n";
|
|
|
|
Sprite sprt;
|
|
string name, tooltip, text;
|
|
bool highlight = false;
|
|
|
|
if(purchaseEmpire !is null) {
|
|
int purchaseCost = getPurchaseCost(purchaseEmpire);
|
|
if(purchaseCost != 0)
|
|
tt += "\n"+formatVariable(icons::Influence,
|
|
locale::CARD_PURCHASE_INFLUENCE, "",
|
|
format(locale::CARD_COST, toString(purchaseCost)), false);
|
|
}
|
|
if(getCostVariable(sprt, name, tooltip, text, highlight))
|
|
tt += "\n"+formatVariable(sprt, name, tooltip, text, highlight);
|
|
if(getWeightVariable(sprt, name, tooltip, text, highlight))
|
|
tt += "\n"+formatVariable(sprt, name, tooltip, text, highlight);
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
auto mode = type.hooks[i].getVariable(this, null, sprt, name, tooltip, text, highlight);
|
|
if(mode != IVM_None)
|
|
tt += "\n"+formatVariable(sprt, name, tooltip, text, highlight);
|
|
}
|
|
|
|
return tt;
|
|
}
|
|
|
|
string formatVariable(const Sprite& sprt, const string& name, const string& tooltip, const string& text, bool highlight) const {
|
|
string value = text;
|
|
if(highlight)
|
|
value = format("[color=#ffcd00][b]$1[/b][/color]", value);
|
|
return format("[img=$3;20/][b] $1[/b][offset=180]$2[/offset]", name, value, getSpriteDesc(sprt));
|
|
}
|
|
|
|
bool getCostVariable(Sprite& sprt, string& name, string& tooltip, string& text, bool& highlight, const InfluenceVote@ vote = null) const {
|
|
int cost = getPlayCost(vote, null);
|
|
if(cost == 0)
|
|
return false;
|
|
sprt = icons::InfluencePlayCost;
|
|
name = locale::CARD_DESC_COST;
|
|
if(cost == INDETERMINATE) {
|
|
text = "~";
|
|
tooltip = locale::INFLUENCE_TT_PLAY_COST_UNKNOWN;
|
|
}
|
|
else {
|
|
if(vote !is null && getSide(null) == ICS_Both) {
|
|
int p = cost + vote.positiveCostPenalty, n = cost + vote.negativeCostPenalty;
|
|
bool canP = vote.canVote(owner, true), canN = vote.canVote(owner, false);
|
|
if(canP && canN && p != n)
|
|
text = toString(min(p,n))+"-"+toString(max(p,n));
|
|
else if(canP)
|
|
text = toString(p);
|
|
else if(canN)
|
|
text = toString(n);
|
|
else
|
|
text = toString(cost);
|
|
}
|
|
else {
|
|
text = toString(cost);
|
|
}
|
|
tooltip = format(locale::INFLUENCE_TT_PLAY_COST, text);
|
|
|
|
}
|
|
highlight = false;
|
|
return true;
|
|
}
|
|
|
|
bool getWeightVariable(Sprite& sprt, string& name, string& tooltip, string& text, bool& highlight, const InfluenceVote@ vote = null) const {
|
|
int weight = getWeight(vote, null);
|
|
if(weight == 0)
|
|
return false;
|
|
sprt = icons::InfluenceWeight;
|
|
name = locale::CARD_DESC_WEIGHT;
|
|
if(weight == INDETERMINATE) {
|
|
text = "~";
|
|
tooltip = locale::INFLUENCE_TT_WEIGHT_UNKNOWN;
|
|
}
|
|
else {
|
|
text = toString(weight);
|
|
tooltip = format(locale::INFLUENCE_TT_WEIGHT, text);
|
|
|
|
}
|
|
highlight = quality > type.minQuality && type.qualityWeight > 0;
|
|
return true;
|
|
}
|
|
|
|
bool checkTargets(const Targets@ check) const {
|
|
if(check is null)
|
|
return true;
|
|
if(check.targets.length != type.targets.targets.length)
|
|
return false;
|
|
for(uint i = 0, cnt = check.targets.length; i < cnt; ++i) {
|
|
if(!isValidTarget(i, check.targets[i]))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//Hooks
|
|
bool canPlay(const Targets@ targets) const {
|
|
if(type.cls == ICC_Support)
|
|
return false;
|
|
if(owner is null)
|
|
return false;
|
|
if(type.leaderOnly && getSenateLeader() !is owner)
|
|
return false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].canPlay(this, targets))
|
|
return false;
|
|
}
|
|
if(!checkTargets(targets))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool isValidTarget(uint index, const Target@ targ) const {
|
|
if(index >= targets.targets.length)
|
|
return false;
|
|
if(targ.type != type.targets.targets[index].type)
|
|
return false;
|
|
if(!targ.filled)
|
|
return false;
|
|
if(targets.targets[index].filled && targets.targets[index] != targ)
|
|
return false;
|
|
if(targ.type == TT_Empire && targ.emp !is null && owner !is null
|
|
&& owner.ContactMask & targ.emp.mask == 0) {
|
|
return false;
|
|
}
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].isValidTarget(this, index, targ))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void targetDefaults(Targets@ targets) const {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].targetDefaults(this, targets);
|
|
}
|
|
|
|
bool getTargetError(uint index, const Target@ targ, string& str) const {
|
|
if(index >= type.targets.length)
|
|
return false;
|
|
if(targ.type != type.targets[index].type)
|
|
return false;
|
|
if(!targ.filled)
|
|
return false;
|
|
if(type.targets[index].filled && type.targets[index] != targ)
|
|
return false;
|
|
if(targ.type == TT_Empire && targ.emp !is null && owner !is null
|
|
&& owner.ContactMask & targ.emp.mask == 0)
|
|
return false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].isValidTarget(this, index, targ)) {
|
|
str = type.hooks[i].getFailReason(this, index, targ);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
string getTargetError(const Targets@ check) const {
|
|
string str;
|
|
for(uint i = 0, cnt = check.targets.length; i < cnt; ++i) {
|
|
if(getTargetError(i, check.targets[i], str))
|
|
return str;
|
|
}
|
|
return str;
|
|
}
|
|
|
|
#section server
|
|
void play(Targets@ targets) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onPlay(this, targets);
|
|
if(uses > 0) {
|
|
uses -= 1;
|
|
lose(1, true);
|
|
}
|
|
}
|
|
|
|
void gain(int uses, bool wasBuy) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onGain(this, uses, wasBuy);
|
|
if(uses > 0) {
|
|
owner.DiplomacyPoints += points * uses;
|
|
owner.points += points * uses;
|
|
}
|
|
if(wasBuy) {
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
auto@ emp = getEmpire(i);
|
|
if(!emp.major)
|
|
continue;
|
|
if(emp is owner)
|
|
continue;
|
|
|
|
double rebate = emp.RebateInfluenceBuys;
|
|
if(rebate != 0)
|
|
emp.addInfluence(+rebate);
|
|
|
|
double copyChance = emp.InfluenceBuysCopyChance;
|
|
if(copyChance != 0 && randomd() >= copyChance)
|
|
emp.gainCard(type.id, 1, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void lose(int uses, bool wasPlayed) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onLose(this, uses, wasPlayed);
|
|
if(uses > 0) {
|
|
owner.DiplomacyPoints -= points * uses;
|
|
owner.points -= points * uses;
|
|
}
|
|
}
|
|
|
|
void enterStack() {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onEnterStack(this);
|
|
}
|
|
|
|
void tickStack(double time) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onStackTick(this, time);
|
|
}
|
|
|
|
void leaveStack(bool wasBuy) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onLeaveStack(this, wasBuy);
|
|
}
|
|
|
|
bool tick(double time) {
|
|
bool changed = false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(type.hooks[i].onTick(this, time))
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
void play(InfluenceVote@ vote, Targets@ targets) {
|
|
auto side = getSide(targets);
|
|
if(side == ICS_Both) {
|
|
throw("Ambiguous side for vote card.");
|
|
return;
|
|
}
|
|
|
|
//Do weight voting
|
|
int weight = 0;
|
|
if(side != ICS_Neutral) {
|
|
weight = getWeight(vote, targets);
|
|
if(side != ICS_Support)
|
|
weight *= -1;
|
|
vote.vote(owner, weight);
|
|
}
|
|
|
|
//Trigger hooks
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onPlay(this, vote, targets, weight);
|
|
|
|
//Check if we should be added as an effect
|
|
bool addEffect = false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(type.hooks[i].isVoteEffect) {
|
|
addEffect = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(addEffect) {
|
|
auto@ eff = vote.addCardEffect(this, targets);
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onVoteEffect(eff, vote);
|
|
}
|
|
|
|
//Remove use
|
|
if(uses > 0) {
|
|
uses -= 1;
|
|
lose(1, true);
|
|
}
|
|
}
|
|
|
|
void voteTick(InfluenceVote@ vote, double time) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onVoteTick(this, vote, time);
|
|
}
|
|
|
|
void voteEvent(InfluenceVote@ vote, InfluenceVoteEvent@ evt) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onVoteEvent(this, vote, evt);
|
|
}
|
|
|
|
void voteEnd(InfluenceVote@ vote, bool passed, bool withdrawn) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onVoteEnd(this, vote, passed, withdrawn);
|
|
}
|
|
#section all
|
|
|
|
InfluenceCardSide getSide(const Targets@ targets) const {
|
|
if(type.sideMode == ICS_Neutral)
|
|
return ICS_Neutral;
|
|
if(type.sideTarget >= 0 && targets !is null) {
|
|
auto@ targ = targets.targets[type.sideTarget];
|
|
if(targ.filled)
|
|
return targ.side ? ICS_Support : ICS_Oppose;
|
|
}
|
|
return type.sideMode;
|
|
}
|
|
|
|
bool canPurchase(Empire@ empire, int placement = 0) const {
|
|
if(type.cls == ICC_Event)
|
|
return false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].canPurchase(this, empire, placement))
|
|
return false;
|
|
}
|
|
for(uint i = 0, cnt = targets.length; i < cnt; ++i) {
|
|
auto@ targ = targets[i];
|
|
if(targ.type == TT_Empire && targ.emp !is null && empire !is null
|
|
&& empire.ContactMask & targ.emp.mask == 0)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool hasPurchaseCost(Empire@ empire, int placement = 0) const {
|
|
if(getPurchaseCost(empire, placement) > empire.Influence)
|
|
return false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].hasPurchaseCost(this, empire, placement))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool canPlay(const InfluenceVote@ vote, const Targets@ targets) const {
|
|
//Can generically always be played
|
|
if(vote is null)
|
|
return true;
|
|
if(owner is null)
|
|
return false;
|
|
if(type.leaderOnly && getSenateLeader() !is owner)
|
|
return false;
|
|
if(!vote.isPresent(owner))
|
|
return false;
|
|
//Only support cards go in votes
|
|
if(type.cls != ICC_Support)
|
|
return false;
|
|
//Check voting limitation
|
|
auto side = getSide(targets);
|
|
switch(side) {
|
|
case ICS_Support:
|
|
if(!vote.canVote(owner, true))
|
|
return false;
|
|
break;
|
|
case ICS_Oppose:
|
|
if(!vote.canVote(owner, false))
|
|
return false;
|
|
break;
|
|
case ICS_Both:
|
|
if(!vote.canVote(owner, false) && !vote.canVote(owner, true))
|
|
return false;
|
|
break;
|
|
}
|
|
//Check hooks
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].canPlay(this, vote, targets))
|
|
return false;
|
|
}
|
|
//Check targets
|
|
if(!checkTargets(targets))
|
|
return false;
|
|
//Check vote
|
|
if(!vote.canPlayOn(this, targets, side))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool canPlayOn(const InfluenceVote@ vote, const InfluenceCard@ other, const Targets@ targets, InfluenceCardSide side) const {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
if(!type.hooks[i].canPlayOn(this, vote, other, targets, side))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool canVoteIn(const InfluenceVote@ vote, Empire& emp, bool support) const {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
if(!type.hooks[i].canVoteIn(this, vote, emp, support))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool purchaseConsume(Empire@ byEmpire, int placement = 0) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].purchaseConsume(this, byEmpire, placement)) {
|
|
for(uint n = 0; n < i; ++n)
|
|
type.hooks[i].purchaseConsumeRewind(this, byEmpire, placement);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool playConsume(Targets@ targets, InfluenceVote@ vote = null) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].playConsume(this, targets, vote)) {
|
|
for(uint n = 0; n < i; ++n)
|
|
type.hooks[i].playConsumeRewind(this, targets, vote);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int getPurchaseCost(Empire@ byEmpire, int placement = 0, int uses = 0) const {
|
|
int cost = type.basePurchaseCost;
|
|
cost += type.placementPurchaseCost * placement;
|
|
cost += type.qualityPurchaseCost * extraQuality;
|
|
if(byEmpire !is null)
|
|
cost += int(byEmpire.ExtraInfluenceBuyCost);
|
|
if(uses == 0) {
|
|
if(this.uses > 1)
|
|
cost += type.usesPurchaseCost * (this.uses - 1);
|
|
}
|
|
else {
|
|
cost += type.usesPurchaseCost * (uses - 1);
|
|
}
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
cost += type.hooks[i].getPurchaseCost(this, byEmpire, placement);
|
|
return cost;
|
|
}
|
|
|
|
int getPlayCost(const InfluenceVote@ vote = null, const Targets@ targets = null) const {
|
|
int cost = type.basePlayCost;
|
|
if(cost == INDETERMINATE)
|
|
return INDETERMINATE;
|
|
int minimum = 0;
|
|
cost += type.qualityPlayCost * extraQuality;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
int mod = type.hooks[i].getPlayCost(this, vote, targets);
|
|
if(mod == INDETERMINATE)
|
|
return INDETERMINATE;
|
|
if(mod < 0)
|
|
minimum = 1;
|
|
cost += mod;
|
|
}
|
|
if(owner !is null) {
|
|
if(type.cls == ICC_Vote)
|
|
cost += owner.VoteCardCostMod;
|
|
else if(type.cls == ICC_Support)
|
|
cost += owner.SupportCardCostMod;
|
|
}
|
|
if(vote !is null && type.cls == ICC_Support) {
|
|
auto side = getSide(targets);
|
|
if(side == ICS_Support)
|
|
cost += vote.positiveCostPenalty;
|
|
else if(side == ICS_Oppose)
|
|
cost += vote.negativeCostPenalty;
|
|
}
|
|
return max(cost, minimum);
|
|
}
|
|
|
|
int getWeight(const InfluenceVote@ vote = null, const Targets@ targets = null) const {
|
|
int weight = type.baseWeight;
|
|
if(weight == INDETERMINATE)
|
|
return INDETERMINATE;
|
|
int minimum = 0;
|
|
weight += type.qualityWeight * extraQuality;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
int mod = type.hooks[i].getWeight(this, vote, targets);
|
|
if(mod == INDETERMINATE)
|
|
return INDETERMINATE;
|
|
if(mod < 0)
|
|
minimum = 1;
|
|
weight += mod;
|
|
}
|
|
return max(weight, minimum);
|
|
}
|
|
|
|
bool canCollapseUses(const InfluenceCard@ other) const {
|
|
if(other.type !is type)
|
|
return false;
|
|
if(other.owner !is owner)
|
|
return false;
|
|
if(targets != other.targets)
|
|
return false;
|
|
if(quality != other.quality)
|
|
return false;
|
|
if(uses <= 0 || other.uses <= 0)
|
|
return false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
if(!type.hooks[i].canCollapseUses(this, other))
|
|
return false;
|
|
return true;
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Effects
|
|
class InfluenceCardEffect : Hook {
|
|
uint hookIndex = 0;
|
|
|
|
void init(InfluenceCardType@ type) {
|
|
}
|
|
|
|
bool canGenerateOnStack() {
|
|
return true;
|
|
}
|
|
|
|
//When a card or use is gained by an empire.
|
|
// wasBuy indicates if the card was bought by the
|
|
// player or gained otherwise.
|
|
void onGain(InfluenceCard@ card, int uses, bool wasBuy) const {
|
|
}
|
|
|
|
//When a card or use is lost by an empire.
|
|
// wasPlayed indicates if the card was lost through being
|
|
// played by the player, or otherwise.
|
|
void onLose(InfluenceCard@ card, int uses, bool wasPlayed) const {
|
|
}
|
|
|
|
//Whether a target is valid
|
|
bool isValidTarget(const InfluenceCard@ card, uint index, const Target@ targets) const {
|
|
return true;
|
|
}
|
|
|
|
string getFailReason(const InfluenceCard@ card, uint index, const Target@ targ) const {
|
|
return "";
|
|
}
|
|
|
|
//Whether to display in the active effects list
|
|
bool isActiveEffect(const InfluenceCard@ card, const InfluenceVoteEvent@ event, const InfluenceVote@ vote) const {
|
|
return false;
|
|
}
|
|
|
|
//Setting the default values for targets. Do not mark them filled.
|
|
void targetDefaults(const InfluenceCard@ card, Targets@ targets) const {
|
|
}
|
|
|
|
//Whether a card can be played
|
|
bool canPlay(const InfluenceCard@ card, const Targets@ targets) const {
|
|
return true;
|
|
}
|
|
|
|
//When a card is triggered, after paying its cost.
|
|
void onPlay(InfluenceCard@ card, Targets@ targets) const {
|
|
}
|
|
|
|
//When a card enters the stack.
|
|
void onEnterStack(InfluenceCard@ card) const {
|
|
}
|
|
|
|
//When a card on the stack ticks.
|
|
void onStackTick(InfluenceCard@ card, double time) const {
|
|
}
|
|
|
|
//When a card leaves the stack.
|
|
void onLeaveStack(InfluenceCard@ card, bool wasBuy) const {
|
|
}
|
|
|
|
//Cards ticking inside the empire's pool. Return true to indicate that
|
|
//something about the card was changed by the tick.
|
|
bool onTick(InfluenceCard@ card, double time) const {
|
|
return false;
|
|
}
|
|
|
|
//Whether this card can be purchased
|
|
bool canPurchase(const InfluenceCard@ card, Empire@ empire, int placement) const {
|
|
return true;
|
|
}
|
|
|
|
//Whether the empire currently has enough to purchase the card
|
|
bool hasPurchaseCost(const InfluenceCard@ card, Empire@ empire, int placement) const {
|
|
return true;
|
|
}
|
|
|
|
//Cost to add to purchasing
|
|
int getPurchaseCost(const InfluenceCard@ card, Empire@ byEmpire, int placement) const {
|
|
return 0;
|
|
}
|
|
|
|
//Consumption on purchasing
|
|
bool purchaseConsume(InfluenceCard@ card, Empire@ byEmpire, int placement) const {
|
|
return true;
|
|
}
|
|
|
|
void purchaseConsumeRewind(InfluenceCard@ card, Empire@ byEmpire, int placement) const {
|
|
}
|
|
|
|
//Consumption on purchasing
|
|
bool playConsume(InfluenceCard@ card, Targets@ targets, InfluenceVote@ vote = null) const {
|
|
return true;
|
|
}
|
|
|
|
void playConsumeRewind(InfluenceCard@ card, Targets@ targets, InfluenceVote@ vote = null) const {
|
|
}
|
|
|
|
//Cost to add to playing
|
|
int getPlayCost(const InfluenceCard@ card, const InfluenceVote@ vote, const Targets@ targets) const {
|
|
return 0;
|
|
}
|
|
|
|
//Variable display on card
|
|
InfluenceVariableMode getVariable(const InfluenceCard@ card, const InfluenceVote@ vote, Sprite& sprt, string& name, string& tooltip, string& text, bool& highlight) const {
|
|
return IVM_None;
|
|
}
|
|
|
|
//Weight to add to voting
|
|
int getWeight(const InfluenceCard@ card, const InfluenceVote@ vote, const Targets@ targets) const {
|
|
return 0;
|
|
}
|
|
|
|
//Whether the card can be played into a vote
|
|
bool canPlay(const InfluenceCard@ card, const InfluenceVote@ vote, const Targets@ targets) const {
|
|
return canPlay(card, targets);
|
|
}
|
|
|
|
//When the card gets played into a vote
|
|
void onPlay(InfluenceCard@ card, InfluenceVote@ vote, Targets@ targets, int weight) const {
|
|
onPlay(card, targets);
|
|
}
|
|
|
|
//Whether this card should receive onVote..() events.
|
|
bool get_isVoteEffect() const {
|
|
return false;
|
|
}
|
|
|
|
//When the card is first placed into a vote as an effect
|
|
void onVoteEffect(InfluenceCard@ card, InfluenceVote@ vote) const {
|
|
}
|
|
|
|
//When the card played into a vote ticks
|
|
void onVoteTick(InfluenceCard@ card, InfluenceVote@ vote, double time) const {
|
|
}
|
|
|
|
//When an event occurs on the vote this card is played in
|
|
void onVoteEvent(InfluenceCard@ card, InfluenceVote@ vote, InfluenceVoteEvent@ event) const {
|
|
}
|
|
|
|
//When the vote this card is played in ends
|
|
void onVoteEnd(InfluenceCard@ card, InfluenceVote@ vote, bool passed, bool withdrawn) const {
|
|
}
|
|
|
|
//Whether a card can be played in the vote this card is in
|
|
bool canPlayOn(const InfluenceCard@ card, const InfluenceVote@ vote, const InfluenceCard@ other, const Targets@ targets, InfluenceCardSide side) const {
|
|
return true;
|
|
}
|
|
|
|
//Whether an empire can vote in the vote this card is in
|
|
bool canVoteIn(const InfluenceCard@ card, const InfluenceVote@ vote, Empire& emp, bool support) const {
|
|
return true;
|
|
}
|
|
|
|
//Formatting for vote events
|
|
bool formatEvent(const InfluenceCard@ card, const InfluenceVoteEvent@ evt, string& text) const {
|
|
return false;
|
|
}
|
|
|
|
//Formatting for notifications
|
|
bool formatNotification(const InfluenceCard@ card, const InfluenceCardPlayEvent@ event, const ICardNotification@ notification, string& text) const {
|
|
return false;
|
|
}
|
|
|
|
//Save hook data
|
|
void save(InfluenceCard@ card, SaveFile& file) const {
|
|
}
|
|
|
|
void save(InfluenceVote@ vote, SaveFile& file) const {
|
|
}
|
|
|
|
//Load hook data
|
|
void load(InfluenceCard@ card, SaveFile& file) const {
|
|
}
|
|
|
|
void load(InfluenceVote@ vote, SaveFile& file) const {
|
|
}
|
|
|
|
//Generate a randomized card
|
|
void generate(InfluenceCard@ card) const {
|
|
}
|
|
|
|
//Whether the uses can be collapsed here
|
|
bool canCollapseUses(const InfluenceCard@ into, const InfluenceCard@ other) const {
|
|
return true;
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{Secondary structures
|
|
tidy final class StackInfluenceCard : InfluenceCard {
|
|
int placement = 0;
|
|
Empire@ purchasedBy;
|
|
|
|
int getPlacement(Empire@ emp) const {
|
|
return max(placement + int(emp.InfluencePlacementMod), 0);
|
|
}
|
|
|
|
int getPurchaseCost(Empire@ byEmpire, int placement = 0, int uses = 0) const override {
|
|
return InfluenceCard::getPurchaseCost(byEmpire, getPlacement(byEmpire), uses);
|
|
}
|
|
|
|
bool purchaseConsume(Empire@ byEmpire, int placement = 0) override {
|
|
return InfluenceCard::purchaseConsume(byEmpire, getPlacement(byEmpire));
|
|
}
|
|
|
|
bool canPurchase(Empire@ empire, int placement = 0) const override {
|
|
if(purchasedBy !is null)
|
|
return false;
|
|
return InfluenceCard::canPurchase(empire, getPlacement(empire));
|
|
}
|
|
|
|
bool hasPurchaseCost(Empire@ empire, int placement = 0) const override {
|
|
if(purchasedBy !is null)
|
|
return false;
|
|
return InfluenceCard::hasPurchaseCost(empire, getPlacement(empire));
|
|
}
|
|
|
|
StackInfluenceCard() {
|
|
super();
|
|
}
|
|
|
|
StackInfluenceCard(Message& msg) {
|
|
read(msg);
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
InfluenceCard::write(msg);
|
|
msg << placement;
|
|
msg << purchasedBy;
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
InfluenceCard::read(msg);
|
|
msg >> placement;
|
|
msg >> purchasedBy;
|
|
}
|
|
|
|
StackInfluenceCard(SaveFile& file) {
|
|
load(file);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
InfluenceCard::save(file);
|
|
file << placement;
|
|
file << purchasedBy;
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
InfluenceCard::load(file);
|
|
file >> placement;
|
|
file >> purchasedBy;
|
|
}
|
|
};
|
|
|
|
interface InfluenceStore {
|
|
int addCard(Empire& emp, InfluenceCard@ card, bool wasBuy = true);
|
|
void playCard(Empire& emp, int id, Targets@ targets, bool pay = true, InfluenceVote@ vote = null);
|
|
};
|
|
|
|
tidy class InfluenceCardPlayEvent : Serializable, Savable {
|
|
InfluenceCard card;
|
|
Targets targets;
|
|
|
|
InfluenceCardPlayEvent() {
|
|
}
|
|
|
|
InfluenceCardPlayEvent(InfluenceCard@ Card, Targets@ Targets) {
|
|
card = Card;
|
|
card.resetData();
|
|
targets = Targets;
|
|
card.targets = Targets;
|
|
}
|
|
|
|
//Networking
|
|
InfluenceCardPlayEvent(Message& msg) {
|
|
read(msg);
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg << card;
|
|
msg << targets;
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
msg >> card;
|
|
msg >> targets;
|
|
}
|
|
|
|
//Saving
|
|
InfluenceCardPlayEvent(SaveFile& file) {
|
|
load(file);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file << card;
|
|
file << targets;
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
file >> card;
|
|
file >> targets;
|
|
}
|
|
|
|
string formatNotification(const ICardNotification@ notification) const {
|
|
string text;
|
|
for(uint i = 0, cnt = card.type.hooks.length; i < cnt; ++i) {
|
|
if(card.type.hooks[i].formatNotification(card, this, notification, text))
|
|
return text;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
bool wasEventOf(const InfluenceCard@ other, bool matchTargets = false) const {
|
|
if(other.id != card.id)
|
|
return false;
|
|
if(other.owner !is card.owner)
|
|
return false;
|
|
if(matchTargets && card.targets != other.targets)
|
|
return false;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
interface ICardNotification {
|
|
};
|
|
//}}}
|
|
//{{{ Initialization
|
|
Sprite getInfluenceCardClassSprite(InfluenceCardClass cls) {
|
|
switch(cls) {
|
|
case ICC_Action: return Sprite(spritesheet::CardCategoryIcons, 0);
|
|
case ICC_Effect: return Sprite(spritesheet::CardCategoryIcons, 1);
|
|
case ICC_Instant: return Sprite(spritesheet::CardCategoryIcons, 2);
|
|
case ICC_Support: return Sprite(spritesheet::CardCategoryIcons, 3);
|
|
case ICC_Vote: return Sprite(spritesheet::CardCategoryIcons, 4);
|
|
case ICC_Event: return Sprite(spritesheet::CardCategoryIcons, 5);
|
|
}
|
|
return Sprite();
|
|
}
|
|
|
|
string getInfluenceCardClassTooltip(InfluenceCardClass cls) {
|
|
switch(cls) {
|
|
case ICC_Action: return locale::INFLUENCE_TT_ACTION;
|
|
case ICC_Instant: return locale::INFLUENCE_TT_INSTANT;
|
|
case ICC_Vote: return locale::INFLUENCE_TT_VOTE;
|
|
case ICC_Effect: return locale::INFLUENCE_TT_EFFECT;
|
|
case ICC_Event: return locale::INFLUENCE_TT_EVENT;
|
|
case ICC_Support: return locale::INFLUENCE_TT_SUPPORT;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
Color getInfluenceCardRarityColor(uint rarity) {
|
|
switch(rarity) {
|
|
case ICR_Basic: case ICR_Common:
|
|
return Color(0xffffffff);
|
|
case ICR_Uncommon: return Color(0x80baffff);
|
|
case ICR_Rare: return Color(0xe580ffff);
|
|
case ICR_Epic: return Color(0xffe680ff);
|
|
}
|
|
return Color(0xffffffff);
|
|
}
|
|
|
|
const array<int> RARITY_DEFAULTS = {10, 4, 3, 2, 1};
|
|
int totalCardFrequency = 0;
|
|
|
|
array<InfluenceCardType@> cardList;
|
|
dictionary cardIdents;
|
|
|
|
uint getInfluenceDeckSize() {
|
|
return totalCardFrequency;
|
|
}
|
|
|
|
uint getInfluenceCardTypeCount() {
|
|
return cardList.length;
|
|
}
|
|
|
|
const InfluenceCardType@ getInfluenceCardType(uint id) {
|
|
if(id >= cardList.length)
|
|
return null;
|
|
return cardList[id];
|
|
}
|
|
|
|
const InfluenceCardType@ getInfluenceCardType(const string& ident) {
|
|
InfluenceCardType@ type;
|
|
cardIdents.get(ident, @type);
|
|
return type;
|
|
}
|
|
|
|
int getInfluenceCardID(const string& ident) {
|
|
InfluenceCardType@ type;
|
|
cardIdents.get(ident, @type);
|
|
if(type is null)
|
|
return -1;
|
|
return type.id;
|
|
}
|
|
|
|
string getInfluenceCardIdent(int id) {
|
|
if(uint(id) >= cardList.length)
|
|
return "";
|
|
return cardList[id].ident;
|
|
}
|
|
|
|
void addInfluenceCardType(InfluenceCardType@ type) {
|
|
type.id = cardList.length;
|
|
cardIdents.set(type.ident, @type);
|
|
cardList.insertLast(type);
|
|
if(type.frequency < 0)
|
|
type.frequency = RARITY_DEFAULTS[type.rarity];
|
|
totalCardFrequency += int(floor(type.frequency));
|
|
if(type.sideMode == ICS_Both)
|
|
type.targets.add("VoteSide", TT_Side);
|
|
for(uint i = 0, cnt = type.targets.targets.length; i < cnt; ++i) {
|
|
if(type.targets.targets[i].name.equals_nocase("voteside")) {
|
|
type.sideTarget = int(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const InfluenceCardType@ getDistributedInfluenceCardType() {
|
|
double num = randomd(0, totalCardFrequency);
|
|
for(uint i = 0, cnt = cardList.length; i < cnt; ++i) {
|
|
auto@ type = cardList[i];
|
|
double freq = floor(type.frequency);
|
|
if(num <= freq)
|
|
return type;
|
|
num -= freq;
|
|
}
|
|
return cardList[cardList.length-1];
|
|
}
|
|
|
|
void readCardLine(const string& line, InfluenceCardType@ type, ReadFile@ file) {
|
|
auto@ hook = cast<InfluenceCardEffect>(parseHook(line, "card_effects::", instantiate=false, file=file));
|
|
if(hook !is null) {
|
|
hook.hookIndex = type.hooks.length;
|
|
type.hooks.insertLast(hook);
|
|
}
|
|
}
|
|
|
|
bool readCard(ReadFile@ file) {
|
|
InfluenceCardType@ type;
|
|
do {
|
|
if(file.fullLine) {
|
|
if(type !is null)
|
|
readCardLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else if(file.key == "Card") {
|
|
if(type !is null)
|
|
addInfluenceCardType(type);
|
|
@type = InfluenceCardType();
|
|
type.ident = file.value;
|
|
}
|
|
else if(file.key == "Name") {
|
|
type.name = localize(file.value);
|
|
}
|
|
else if(file.key == "Description") {
|
|
type.description = localize(file.value);
|
|
}
|
|
else if(file.key == "Color") {
|
|
type.color = toColor(file.value);
|
|
}
|
|
else if(file.key == "Icon") {
|
|
type.icon = getSprite(file.value);
|
|
}
|
|
else if(file.key == "DLC") {
|
|
type.dlc = file.value;
|
|
}
|
|
else if(file.key == "Frequency") {
|
|
type.frequency = toDouble(file.value);
|
|
}
|
|
else if(file.key == "AI") {
|
|
auto@ hook = parseHook(file.value, "ai.diplomacy::", instantiate=false, file=file);
|
|
if(hook !is null)
|
|
type.ai.insertLast(hook);
|
|
}
|
|
else if(file.key == "Collapse Uses") {
|
|
type.collapseUses = toBool(file.value);
|
|
}
|
|
else if(file.key == "Points") {
|
|
type.points = toUInt(file.value);
|
|
}
|
|
else if(file.key == "Leader Only") {
|
|
type.leaderOnly = toBool(file.value);
|
|
}
|
|
else if(file.key == "Class") {
|
|
if(file.value.equals_nocase("support")) {
|
|
type.cls = ICC_Support;
|
|
}
|
|
else if(file.value.equals_nocase("vote")) {
|
|
type.cls = ICC_Vote;
|
|
}
|
|
else if(file.value.equals_nocase("effect")) {
|
|
type.cls = ICC_Effect;
|
|
}
|
|
else if(file.value.equals_nocase("action")) {
|
|
type.cls = ICC_Action;
|
|
}
|
|
else if(file.value.equals_nocase("event")) {
|
|
type.cls = ICC_Event;
|
|
}
|
|
else if(file.value.equals_nocase("instant")) {
|
|
type.cls = ICC_Instant;
|
|
}
|
|
else if(file.value.equals_nocase("misc")) {
|
|
type.cls = ICC_Misc;
|
|
}
|
|
else {
|
|
file.error("Invalid card class: "+file.value);
|
|
}
|
|
}
|
|
else if(file.key == "Rarity") {
|
|
if(file.value.equals_nocase("basic")) {
|
|
type.rarity = ICR_Basic;
|
|
}
|
|
else if(file.value.equals_nocase("common")) {
|
|
type.rarity = ICR_Common;
|
|
}
|
|
else if(file.value.equals_nocase("uncommon")) {
|
|
type.rarity = ICR_Uncommon;
|
|
}
|
|
else if(file.value.equals_nocase("rare")) {
|
|
type.rarity = ICR_Rare;
|
|
}
|
|
else if(file.value.equals_nocase("epic")) {
|
|
type.rarity = ICR_Epic;
|
|
}
|
|
else {
|
|
file.error("Invalid card rarity: "+file.value);
|
|
}
|
|
}
|
|
else if(file.key == "Side") {
|
|
if(file.value.equals_nocase("both")) {
|
|
type.sideMode = ICS_Both;
|
|
}
|
|
else if(file.value.equals_nocase("support")) {
|
|
type.sideMode = ICS_Support;
|
|
}
|
|
else if(file.value.equals_nocase("oppose")) {
|
|
type.sideMode = ICS_Oppose;
|
|
}
|
|
else if(file.value.equals_nocase("neutral")) {
|
|
type.sideMode = ICS_Neutral;
|
|
}
|
|
else {
|
|
file.error("Invalid card side: "+file.value);
|
|
}
|
|
}
|
|
else if(file.key == "Base Purchase Cost") {
|
|
type.basePurchaseCost = toInt(file.value);
|
|
}
|
|
else if(file.key == "Quality Purchase Cost") {
|
|
type.qualityPurchaseCost = toInt(file.value);
|
|
}
|
|
else if(file.key == "Placement Purchase Cost") {
|
|
type.placementPurchaseCost = toInt(file.value);
|
|
}
|
|
else if(file.key == "Uses Purchase Cost") {
|
|
type.usesPurchaseCost = toInt(file.value);
|
|
}
|
|
else if(file.key == "Base Play Cost") {
|
|
type.basePlayCost = toInt(file.value);
|
|
}
|
|
else if(file.key == "Quality Play Cost") {
|
|
type.qualityPlayCost = toInt(file.value);
|
|
}
|
|
else if(file.key == "Base Weight") {
|
|
type.baseWeight = toInt(file.value);
|
|
}
|
|
else if(file.key == "Quality Weight") {
|
|
type.qualityWeight = toInt(file.value);
|
|
}
|
|
else if(file.key == "Min Quality") {
|
|
type.minQuality = toInt(file.value);
|
|
}
|
|
else if(file.key == "Max Quality") {
|
|
type.maxQuality = toInt(file.value);
|
|
}
|
|
else if(file.key == "Can Overquality") {
|
|
type.canOverquality = toBool(file.value);
|
|
}
|
|
else if(file.key == "Min Uses") {
|
|
type.minUses = toInt(file.value);
|
|
}
|
|
else if(file.key == "Max Uses") {
|
|
type.maxUses = toInt(file.value);
|
|
}
|
|
else if(file.key == "Target") {
|
|
parseTarget(type.targets, file.value);
|
|
}
|
|
else {
|
|
if(file.line.findFirst("(") != -1) {
|
|
if(type !is null)
|
|
readCardLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else {
|
|
if(type !is null)
|
|
addInfluenceCardType(type);
|
|
return false;
|
|
}
|
|
}
|
|
} while(file++);
|
|
|
|
if(type !is null)
|
|
addInfluenceCardType(type);
|
|
return true;
|
|
}
|
|
//}}}
|
|
// }}}
|
|
// {{{ Influence votes
|
|
export InfluenceVoteType;
|
|
export getInfluenceVoteTypeCount;
|
|
export getInfluenceVoteType;
|
|
|
|
export InfluenceVote;
|
|
export InfluenceVoteStub;
|
|
export InfluenceVoteEventType;
|
|
export InfluenceVoteEvent;
|
|
export InfluenceVoteEffect;
|
|
|
|
//{{{ Types
|
|
tidy final class InfluenceVoteType {
|
|
uint id;
|
|
string ident;
|
|
string name;
|
|
string description;
|
|
Sprite icon;
|
|
Color color;
|
|
|
|
Targets targets;
|
|
array<InfluenceVoteEffect@> hooks;
|
|
array<Hook@> ai;
|
|
|
|
void init() {
|
|
for(uint i = 0, cnt = hooks.length; i < cnt; ++i) {
|
|
if(!hooks[i].instantiate())
|
|
error("Could not instantiate hook: "+addrstr(hooks[i])+" in "+ident);
|
|
hooks[i].initTargets(targets);
|
|
hooks[i].init(this);
|
|
}
|
|
for(uint i = 0, cnt = ai.length; i < cnt; ++i) {
|
|
if(!ai[i].instantiate())
|
|
error("Could not instantiate AI hook: "+addrstr(ai[i])+" in vote "+ident);
|
|
ai[i].initTargets(targets);
|
|
}
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Votes
|
|
tidy final class InfluenceVote : Serializable, Savable {
|
|
const InfluenceVoteType@ type;
|
|
Targets targets;
|
|
|
|
uint id = uint(-1);
|
|
array<any> data;
|
|
|
|
Empire@ startedBy;
|
|
double startedAt = -1.0;
|
|
double endedAt = -1.0;
|
|
double currentTime = 0;
|
|
bool active = false;
|
|
bool succeeded = false;
|
|
bool delta = false;
|
|
|
|
double positiveSpeed = 1.0;
|
|
double negativeSpeed = 1.0;
|
|
int positiveCostPenalty = 0;
|
|
int negativeCostPenalty = 0;
|
|
|
|
uint empiresPresent = ~0;
|
|
array<int> empireVotes(getEmpireCount(), 0);
|
|
array<double> contribPoints(getEmpireCount(), 0);
|
|
|
|
array<InfluenceVoteOffer@> offers;
|
|
uint nextOfferId = 0;
|
|
|
|
int totalFor = 0;
|
|
int totalAgainst = 0;
|
|
|
|
array<InfluenceVoteEvent@> events;
|
|
array<InfluenceCard@> effects;
|
|
|
|
InfluenceVote() {
|
|
}
|
|
|
|
InfluenceVote(const InfluenceVoteType@ Type) {
|
|
set(Type);
|
|
}
|
|
|
|
void set(const InfluenceVoteType@ Type) {
|
|
@type = Type;
|
|
targets.set(type.targets);
|
|
data.length = type.hooks.length;
|
|
}
|
|
|
|
bool checkTargets(const Targets@ check) const {
|
|
if(check is null)
|
|
return true;
|
|
if(check.targets.length != type.targets.targets.length)
|
|
return false;
|
|
for(uint i = 0, cnt = check.targets.length; i < cnt; ++i) {
|
|
if(!check.targets[i].filled)
|
|
return false;
|
|
if(check.targets[i].type != type.targets.targets[i].type)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//Handling
|
|
bool isPresent(Empire& emp) const {
|
|
return empiresPresent & emp.mask != 0;
|
|
}
|
|
|
|
int get_totalVote() const {
|
|
return totalFor - totalAgainst;
|
|
}
|
|
|
|
double get_minimumRemaining() const {
|
|
if(currentTime < 0.0)
|
|
return -config::INFLUENCE_FAIL_THRES - currentTime;
|
|
else
|
|
return config::INFLUENCE_PASS_THRES - currentTime;
|
|
}
|
|
|
|
double get_remainingTime() const {
|
|
if(totalFor > totalAgainst) {
|
|
double speed = positiveSpeed;
|
|
if(speed < 0.0001)
|
|
speed *= 100000.0;
|
|
return (config::INFLUENCE_PASS_THRES-currentTime) / speed;
|
|
}
|
|
else {
|
|
double speed = negativeSpeed;
|
|
if(speed < 0.0001)
|
|
speed *= 100000.0;
|
|
return (currentTime-config::INFLUENCE_FAIL_THRES) / speed;
|
|
}
|
|
}
|
|
|
|
Empire@ get_highestContributor() const {
|
|
Empire@ emp;
|
|
int highest = INT_MIN;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
int amt = empireVotes[i];
|
|
if(amt > highest) {
|
|
@emp = getEmpire(i);
|
|
highest = amt;
|
|
}
|
|
}
|
|
return emp;
|
|
}
|
|
|
|
Empire@ get_lowestContributor() const {
|
|
Empire@ emp;
|
|
int lowest = INT_MAX;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
int amt = empireVotes[i];
|
|
if(amt < lowest) {
|
|
@emp = getEmpire(i);
|
|
lowest = amt;
|
|
}
|
|
}
|
|
return emp;
|
|
}
|
|
|
|
Empire@ get_highestContribPoints() const {
|
|
Empire@ emp;
|
|
double highest = -INFINITY;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
double amt = contribPoints[i];
|
|
if(amt > highest) {
|
|
@emp = getEmpire(i);
|
|
highest = amt;
|
|
}
|
|
}
|
|
return emp;
|
|
}
|
|
|
|
Empire@ get_lowestContribPoints() const {
|
|
Empire@ emp;
|
|
double lowest = INFINITY;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
double amt = contribPoints[i];
|
|
if(amt < lowest) {
|
|
@emp = getEmpire(i);
|
|
lowest = amt;
|
|
}
|
|
}
|
|
return emp;
|
|
}
|
|
|
|
double get_highestContribPointsValue() const {
|
|
double highest = -INFINITY;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
double amt = contribPoints[i];
|
|
if(amt > highest)
|
|
highest = amt;
|
|
}
|
|
return highest;
|
|
}
|
|
|
|
double get_lowestContribPointsValue() const {
|
|
double lowest = INFINITY;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
double amt = contribPoints[i];
|
|
if(amt < lowest)
|
|
lowest = amt;
|
|
}
|
|
return lowest;
|
|
}
|
|
|
|
double getContribPoints(Empire& emp) const {
|
|
if(!emp.valid)
|
|
return 0;
|
|
return contribPoints[emp.index];
|
|
}
|
|
|
|
bool canVote(Empire& emp, bool support) const {
|
|
if(emp is startedBy && !support)
|
|
return false;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].canVote(this, emp, support))
|
|
return false;
|
|
}
|
|
for(uint i = 0, cnt = effects.length; i < cnt; ++i) {
|
|
if(!effects[i].canVoteIn(this, emp, support))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool canPlayOn(const InfluenceCard@ card, const Targets@ targets, InfluenceCardSide side) const {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].canPlayOn(this, card, targets, side))
|
|
return false;
|
|
}
|
|
for(uint i = 0, cnt = effects.length; i < cnt; ++i) {
|
|
if(!effects[i].canPlayOn(this, card, targets, side))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int getVoteFrom(Empire& emp) const {
|
|
if(!emp.valid)
|
|
return 0;
|
|
return empireVotes[emp.index];
|
|
}
|
|
|
|
int getVoteFrom(Empire& emp, InfluenceCardSide side) const {
|
|
if(!emp.valid)
|
|
return 0;
|
|
int vote = empireVotes[emp.index];
|
|
if(side == ICS_Both)
|
|
return abs(vote);
|
|
if(vote < 0 && side == ICS_Oppose)
|
|
return -vote;
|
|
if(vote > 0 && side == ICS_Support)
|
|
return vote;
|
|
return 0;
|
|
}
|
|
|
|
uint countPlayed(const InfluenceCardType@ type, Empire@ matchEmpire = null, InfluenceCardSide matchSide = ICS_Both, const Targets@ matchTargets = null) const {
|
|
uint count = 0;
|
|
for(uint i = 0, cnt = events.length; i < cnt; ++i) {
|
|
auto evt = events[i];
|
|
if(evt.type != IVET_Card)
|
|
continue;
|
|
if(evt.cardEvent.card.type !is type)
|
|
continue;
|
|
if(matchEmpire !is null && evt.cardEvent.card.owner !is matchEmpire)
|
|
continue;
|
|
if(type.sideMode == ICS_Both && matchSide != ICS_Both) {
|
|
if(evt.cardEvent.targets.targets[type.sideTarget].side != (matchSide == ICS_Support))
|
|
continue;
|
|
}
|
|
if(matchTargets !is null && matchTargets != evt.cardEvent.targets)
|
|
continue;
|
|
count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool hasActiveEffect(uint effId) const {
|
|
for(uint i = 0, cnt = effects.length; i < cnt; ++i) {
|
|
if(effects[i].type.id == effId)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#section server
|
|
void vote(Empire& emp, int amount) {
|
|
if(!emp.valid)
|
|
return;
|
|
empireVotes[emp.index] += amount;
|
|
if(amount > 0)
|
|
totalFor += amount;
|
|
else if(amount < 0)
|
|
totalAgainst -= amount;
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(offers[i].side == amount > 0)
|
|
offers[i].claims[emp.index] += abs(amount);
|
|
}
|
|
delta = true;
|
|
}
|
|
|
|
void extendTo(double minimum) {
|
|
if(minimumRemaining >= minimum)
|
|
return;
|
|
|
|
double posTime = (config::INFLUENCE_PASS_THRES - minimum) / positiveSpeed;
|
|
double negTime = (config::INFLUENCE_FAIL_THRES + minimum) / negativeSpeed;
|
|
currentTime = clamp(currentTime, negTime, posTime);
|
|
}
|
|
|
|
void leave(Empire& emp) {
|
|
if(!emp.valid)
|
|
return;
|
|
empiresPresent &= ~emp.mask;
|
|
int vote = empireVotes[emp.index];
|
|
if(vote > 0)
|
|
totalFor -= vote;
|
|
else if(vote < 0)
|
|
totalAgainst += vote;
|
|
empireVotes[emp.index] = 0;
|
|
delta = true;
|
|
}
|
|
|
|
bool makeOffer(InfluenceVoteOffer@ offer) {
|
|
if(!offer.canOffer())
|
|
return false;
|
|
if(!offer.take())
|
|
return false;
|
|
offer.id = nextOfferId++;
|
|
offers.insertLast(offer);
|
|
addOfferEvent(IVET_MakeOffer, offer.fromEmpire, offer);
|
|
delta = true;
|
|
return true;
|
|
}
|
|
|
|
bool claimOffer(Empire@ emp, int id) {
|
|
if(emp is null || !emp.valid)
|
|
return false;
|
|
InfluenceVoteOffer@ off;
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(offers[i].id == id) {
|
|
@off = offers[i];
|
|
break;
|
|
}
|
|
}
|
|
if(off is null)
|
|
return false;
|
|
if(off.claims[emp.index] < off.support)
|
|
return false;
|
|
if(off.fromEmpire is emp)
|
|
return false;
|
|
|
|
addOfferEvent(IVET_ClaimOffer, emp, off);
|
|
off.give(emp);
|
|
|
|
int maxRemaining = off.claims[emp.index] - off.support;
|
|
int found = -1;
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(offers[i] is off) {
|
|
found = int(i);
|
|
continue;
|
|
}
|
|
if(found == -1)
|
|
offers[i].claims[emp.index] -= off.support;
|
|
else
|
|
offers[i].claims[emp.index] = min(maxRemaining, offers[i].claims[emp.index]);
|
|
}
|
|
offers.removeAt(uint(found));
|
|
|
|
delta = true;
|
|
return true;
|
|
}
|
|
|
|
void addEvent(InfluenceVoteEvent@ evt) {
|
|
if(evt.visionMask == 0)
|
|
evt.visionMask = empiresPresent;
|
|
events.insertLast(evt);
|
|
delta = true;
|
|
|
|
//Inform cards
|
|
for(uint i = 0, cnt = effects.length; i < cnt; ++i)
|
|
effects[i].voteEvent(this, evt);
|
|
|
|
//Make notification
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
Empire@ emp = getEmpire(i);
|
|
if(!emp.major)
|
|
continue;
|
|
if(empiresPresent & emp.mask == 0)
|
|
continue;
|
|
emp.notifyVote(id, events.length - 1);
|
|
}
|
|
}
|
|
|
|
//Add a card event to the log, the card's state is stored for future formatting purposes.
|
|
InfluenceVoteEvent@ addCardEvent(InfluenceCard@ card, Targets@ targets, int weight, uint eventType = 0, uint visionMask = ~0) {
|
|
InfluenceVoteEvent evt(IVET_Card, card.owner);
|
|
@evt.cardEvent = InfluenceCardPlayEvent(card, targets);
|
|
evt.weight = weight;
|
|
evt.visionMask = visionMask;
|
|
evt.eventType = eventType;
|
|
addEvent(evt);
|
|
return evt;
|
|
}
|
|
|
|
//Add an offer event to the log
|
|
InfluenceVoteEvent@ addOfferEvent(InfluenceVoteEventType type, Empire@ empire, InfluenceVoteOffer@ offer) {
|
|
InfluenceVoteEvent evt(type, empire);
|
|
@evt.offer = InfluenceVoteOffer();
|
|
evt.offer = offer;
|
|
evt.offer.memo();
|
|
|
|
addEvent(evt);
|
|
return evt;
|
|
}
|
|
|
|
//Add a card effect to the vote, the effect will be informed of things happening to the vote.
|
|
// The current state of the card is copied over into the new effect.
|
|
InfluenceCard@ addCardEffect(InfluenceCard@ card, Targets@ targets) {
|
|
InfluenceCard effect = card;
|
|
effect.targets = targets;
|
|
effects.insertLast(effect);
|
|
delta = true;
|
|
return effect;
|
|
}
|
|
|
|
void removeCardEffect(InfluenceCard@ effect) {
|
|
effects.remove(effect);
|
|
delta = true;
|
|
}
|
|
|
|
//Hooks
|
|
void start(Targets@ targets, Empire@ byEmpire, InfluenceCard@ fromCard = null) {
|
|
@startedBy = byEmpire;
|
|
targets = targets;
|
|
startedAt = gameTime;
|
|
active = true;
|
|
delta = true;
|
|
vote(byEmpire, 1 + int(byEmpire.VoteStartSupport));
|
|
if(active) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onStart(this);
|
|
}
|
|
if(fromCard !is null) {
|
|
auto@ evt = addCardEvent(fromCard, fromCard.targets, 0);
|
|
evt.type = IVET_Start;
|
|
}
|
|
else
|
|
addEvent(InfluenceVoteEvent(IVET_Start, startedBy));
|
|
}
|
|
|
|
void tick(double time) {
|
|
//Handle timer update
|
|
if(totalFor > totalAgainst)
|
|
currentTime += time * positiveSpeed;
|
|
else
|
|
currentTime -= time * negativeSpeed;
|
|
|
|
//Tick all the hooks
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(type.hooks[i].onTick(this, time))
|
|
delta = true;
|
|
}
|
|
|
|
if(!active)
|
|
return;
|
|
|
|
//Tick cards
|
|
for(uint i = 0, cnt = effects.length; i < cnt; ++i)
|
|
effects[i].voteTick(this, time);
|
|
|
|
//Check offers
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(!offers[i].valid) {
|
|
offers[i].invalidate();
|
|
offers.removeAt(i);
|
|
--i; --cnt;
|
|
}
|
|
}
|
|
|
|
//Tally contribution points
|
|
for(uint i = 0, cnt = contribPoints.length; i < cnt; ++i)
|
|
contribPoints[i] += double(empireVotes[i]) * time;
|
|
|
|
//Figure out if we should end
|
|
if(currentTime <= config::INFLUENCE_FAIL_THRES)
|
|
end(false, false);
|
|
else if(currentTime >= config::INFLUENCE_PASS_THRES)
|
|
end(true, false);
|
|
}
|
|
|
|
void withdraw() {
|
|
addEvent(InfluenceVoteEvent(IVET_Withdraw, startedBy));
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(!offers[i].side) {
|
|
offers[i].give(startedBy);
|
|
offers.removeAt(i);
|
|
--i; --cnt;
|
|
}
|
|
}
|
|
end(false, true);
|
|
}
|
|
|
|
void end(bool passed, bool withdrawn) {
|
|
delta = true;
|
|
active = false;
|
|
endedAt = gameTime;
|
|
succeeded = passed;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onEnd(this, passed, withdrawn);
|
|
for(uint i = 0, cnt = effects.length; i < cnt; ++i)
|
|
effects[i].voteEnd(this, passed, withdrawn);
|
|
if(passed) {
|
|
currentTime = config::INFLUENCE_PASS_THRES;
|
|
addEvent(InfluenceVoteEvent(IVET_Pass));
|
|
}
|
|
else {
|
|
currentTime = config::INFLUENCE_FAIL_THRES;
|
|
addEvent(InfluenceVoteEvent(IVET_Fail));
|
|
}
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i)
|
|
offers[i].expire();
|
|
offers.length = 0;
|
|
}
|
|
#section shadow
|
|
void tick(double time) {
|
|
//Handle timer update
|
|
if(totalFor > totalAgainst)
|
|
currentTime += time;
|
|
else
|
|
currentTime -= time;
|
|
}
|
|
#section all
|
|
|
|
//Networking
|
|
InfluenceVote(Message& msg) {
|
|
read(msg);
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg << type.id;
|
|
targets.writeData(msg, type.targets);
|
|
msg << id;
|
|
msg << startedBy;
|
|
msg << startedAt;
|
|
msg << endedAt;
|
|
msg << currentTime;
|
|
msg << active;
|
|
msg << succeeded;
|
|
msg << empiresPresent;
|
|
msg << totalFor;
|
|
msg << totalAgainst;
|
|
msg << positiveSpeed;
|
|
msg << negativeSpeed;
|
|
msg << positiveCostPenalty;
|
|
msg << negativeCostPenalty;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
msg << empireVotes[i];
|
|
msg.writeBit(contribPoints[i] != 0);
|
|
if(contribPoints[i] != 0)
|
|
msg << float(contribPoints[i]);
|
|
}
|
|
uint cnt = offers.length;
|
|
msg << cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
msg << offers[i];
|
|
msg << nextOfferId;
|
|
cnt = effects.length;
|
|
msg.writeSmall(cnt);
|
|
for(uint i = 0; i < cnt; ++i)
|
|
msg << effects[i];
|
|
writeEvents(msg, null);
|
|
}
|
|
|
|
void write(Message& msg, Empire@ forEmpire) {
|
|
write(msg);
|
|
writeEvents(msg, forEmpire);
|
|
}
|
|
|
|
void writeEvents(Message& msg, Empire@ forEmpire) {
|
|
msg.writeAlign();
|
|
uint pos = msg.reserve();
|
|
uint n = 0;
|
|
for(uint i = 0, cnt = events.length; i < cnt; ++i) {
|
|
if(!events[i].visibleTo(forEmpire))
|
|
continue;
|
|
|
|
msg << events[i];
|
|
++n;
|
|
}
|
|
msg.fill(pos, n);
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
uint typeId = 0;
|
|
msg >> typeId;
|
|
@type = getInfluenceVoteType(typeId);
|
|
targets.readData(msg, type.targets);
|
|
data.length = type.hooks.length;
|
|
msg >> id;
|
|
msg >> startedBy;
|
|
msg >> startedAt;
|
|
msg >> endedAt;
|
|
msg >> currentTime;
|
|
msg >> active;
|
|
msg >> succeeded;
|
|
msg >> empiresPresent;
|
|
msg >> totalFor;
|
|
msg >> totalAgainst;
|
|
msg >> positiveSpeed;
|
|
msg >> negativeSpeed;
|
|
msg >> positiveCostPenalty;
|
|
msg >> negativeCostPenalty;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
msg >> empireVotes[i];
|
|
if(msg.readBit())
|
|
contribPoints[i] = msg.read_float();
|
|
else
|
|
contribPoints[i] = 0;
|
|
}
|
|
|
|
uint cnt = 0;
|
|
msg >> cnt;
|
|
offers.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
if(offers[i] is null)
|
|
@offers[i] = InfluenceVoteOffer();
|
|
msg >> offers[i];
|
|
}
|
|
msg >> nextOfferId;
|
|
|
|
cnt = msg.readSmall();
|
|
effects.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
if(effects[i] is null)
|
|
@effects[i] = InfluenceCard();
|
|
msg >> effects[i];
|
|
}
|
|
|
|
msg.readAlign();
|
|
uint evCnt = 0;
|
|
msg >> evCnt;
|
|
events.length = evCnt;
|
|
for(uint i = 0; i < evCnt; ++i) {
|
|
if(events[i] !is null)
|
|
events[i].read(msg);
|
|
else
|
|
@events[i] = InfluenceVoteEvent(msg);
|
|
}
|
|
}
|
|
|
|
//Saving
|
|
InfluenceVote(SaveFile& file) {
|
|
load(file);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file.writeIdentifier(SI_InfluenceVote, type.id);
|
|
targets.saveData(file, type.targets);
|
|
file << id;
|
|
file << startedBy;
|
|
file << startedAt;
|
|
file << endedAt;
|
|
file << currentTime;
|
|
file << active;
|
|
file << succeeded;
|
|
file << empiresPresent;
|
|
file << totalFor;
|
|
file << totalAgainst;
|
|
file << positiveSpeed;
|
|
file << negativeSpeed;
|
|
file << positiveCostPenalty;
|
|
file << negativeCostPenalty;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
file << empireVotes[i];
|
|
file << contribPoints[i];
|
|
}
|
|
|
|
uint cnt = events.length;
|
|
file << cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
file << events[i];
|
|
|
|
cnt = effects.length;
|
|
file << cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
file << effects[i];
|
|
|
|
cnt = offers.length;
|
|
file << cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
file << offers[i];
|
|
file << nextOfferId;
|
|
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].save(this, file);
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
uint typeId = file.readIdentifier(SI_InfluenceVote);
|
|
@type = getInfluenceVoteType(typeId);
|
|
data.length = type.hooks.length;
|
|
targets.loadData(file, type.targets);
|
|
file >> id;
|
|
file >> startedBy;
|
|
file >> startedAt;
|
|
file >> endedAt;
|
|
file >> currentTime;
|
|
file >> active;
|
|
file >> succeeded;
|
|
file >> empiresPresent;
|
|
file >> totalFor;
|
|
file >> totalAgainst;
|
|
if(file >= SV_0080) {
|
|
file >> positiveSpeed;
|
|
file >> negativeSpeed;
|
|
file >> positiveCostPenalty;
|
|
file >> negativeCostPenalty;
|
|
}
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
file >> empireVotes[i];
|
|
if(file >= SV_0099)
|
|
file >> contribPoints[i];
|
|
}
|
|
|
|
uint cnt = 0;
|
|
file >> cnt;
|
|
events.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
@events[i] = InfluenceVoteEvent(file);
|
|
|
|
file >> cnt;
|
|
effects.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
@effects[i] = InfluenceCard(file);
|
|
|
|
if(file >= SV_0055) {
|
|
file >> cnt;
|
|
offers.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
if(offers[i] is null)
|
|
@offers[i] = InfluenceVoteOffer();
|
|
file >> offers[i];
|
|
}
|
|
file >> nextOfferId;
|
|
}
|
|
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].load(this, file);
|
|
}
|
|
|
|
//Formatting
|
|
string formatTitle() const {
|
|
return targets.format(type.name);
|
|
}
|
|
|
|
string formatDescription() const {
|
|
return targets.format(type.description);
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Effects
|
|
class InfluenceVoteEffect : Hook {
|
|
uint hookIndex = 0;
|
|
|
|
void init(InfluenceVoteType@ type) {
|
|
}
|
|
|
|
//When the vote is started
|
|
void onStart(InfluenceVote@ vote) const {
|
|
}
|
|
|
|
//Every time the vote ticks
|
|
bool onTick(InfluenceVote@ vote, double time) const {
|
|
return false;
|
|
}
|
|
|
|
//Right after the vote ends
|
|
void onEnd(InfluenceVote@ vote, bool passed, bool withdrawn) const {
|
|
}
|
|
|
|
//Whether an empire can add weight for a side
|
|
bool canVote(const InfluenceVote@ vote, Empire& emp, bool support) const {
|
|
return true;
|
|
}
|
|
|
|
//Whether a card can be played
|
|
bool canPlayOn(const InfluenceVote@ vote, const InfluenceCard@ card, const Targets@ targets, InfluenceCardSide side) const {
|
|
return true;
|
|
}
|
|
|
|
//Save hook data
|
|
void save(InfluenceVote@ vote, SaveFile& file) const {
|
|
}
|
|
|
|
//Load hook data
|
|
void load(InfluenceVote@ vote, SaveFile& file) const {
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Events
|
|
enum InfluenceVoteEventType {
|
|
IVET_Start,
|
|
IVET_Leave,
|
|
IVET_Card,
|
|
IVET_Withdraw,
|
|
IVET_Pass,
|
|
IVET_Fail,
|
|
IVET_Message,
|
|
IVET_MakeOffer,
|
|
IVET_ClaimOffer,
|
|
|
|
IVET_COUNT
|
|
};
|
|
|
|
tidy final class InfluenceVoteStub : Serializable, Savable {
|
|
uint id = uint(-1);
|
|
const InfluenceVoteType@ type;
|
|
Empire@ startedBy;
|
|
Targets targets;
|
|
|
|
InfluenceVoteStub() {
|
|
}
|
|
|
|
InfluenceVoteStub(const InfluenceVote@ vote) {
|
|
id = vote.id;
|
|
@type = vote.type;
|
|
@startedBy = vote.startedBy;
|
|
targets.set(vote.targets);
|
|
}
|
|
|
|
InfluenceVoteStub(Message& msg) {
|
|
read(msg);
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg << type.id;
|
|
msg << startedBy;
|
|
msg << id;
|
|
msg << targets;
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
uint typeId = 0;
|
|
msg >> typeId;
|
|
@type = getInfluenceVoteType(typeId);
|
|
msg >> startedBy;
|
|
msg >> id;
|
|
targets.read(msg);
|
|
}
|
|
|
|
InfluenceVoteStub(SaveFile& file) {
|
|
load(file);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file.writeIdentifier(SI_InfluenceVote, type.id);
|
|
file << startedBy;
|
|
file << id;
|
|
file << targets;
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
@type = getInfluenceVoteType(file.readIdentifier(SI_InfluenceVote));
|
|
file >> startedBy;
|
|
file >> id;
|
|
targets.load(file);
|
|
}
|
|
|
|
string formatTitle() const{
|
|
return targets.format(type.name);
|
|
}
|
|
};
|
|
|
|
tidy final class InfluenceVoteEvent : Serializable, Savable {
|
|
InfluenceVoteEventType type = IVET_COUNT;
|
|
Empire@ emp;
|
|
string text;
|
|
double time;
|
|
uint eventType = 0;
|
|
int weight = 0;
|
|
uint visionMask = 0;
|
|
InfluenceCardPlayEvent@ cardEvent;
|
|
InfluenceVoteOffer@ offer;
|
|
|
|
InfluenceVoteEvent() {
|
|
}
|
|
|
|
InfluenceVoteEvent(InfluenceVoteEventType Type, Empire@ empire = null) {
|
|
type = Type;
|
|
@emp = empire;
|
|
time = gameTime;
|
|
}
|
|
|
|
//Formatting
|
|
string formatEvent() const {
|
|
switch(type) {
|
|
case IVET_Start:
|
|
if(cardEvent !is null)
|
|
return format(locale::VOTE_LOG_START_CARD, formatEmpireName(emp, contactCheck=playerEmpire),
|
|
cardEvent.card.formatTitle(), toString(cardEvent.card.type.color));
|
|
else
|
|
return format(locale::VOTE_LOG_START, formatEmpireName(emp, contactCheck=playerEmpire));
|
|
case IVET_Leave:
|
|
return format(locale::VOTE_LOG_LEAVE, formatEmpireName(emp, contactCheck=playerEmpire));
|
|
case IVET_Withdraw:
|
|
return format(locale::VOTE_LOG_WITHDRAW, formatEmpireName(emp, contactCheck=playerEmpire));
|
|
case IVET_Pass:
|
|
return locale::VOTE_LOG_SUCCESS;
|
|
case IVET_Fail:
|
|
return locale::VOTE_LOG_FAIL;
|
|
case IVET_Message:
|
|
return format(locale::VOTE_LOG_MESSAGE, formatEmpireName(emp, contactCheck=playerEmpire), text);
|
|
case IVET_MakeOffer:
|
|
if(offer is null)
|
|
return "----";
|
|
return format(offer.side ? locale::LOG_OFFER_FOR : locale::LOG_OFFER_AGAINST,
|
|
formatEmpireName(emp, contactCheck=playerEmpire), offer.blurb, toString(offer.support));
|
|
case IVET_ClaimOffer:
|
|
if(offer is null)
|
|
return "----";
|
|
return format(offer.side ? locale::LOG_CLAIM_OFFER_FOR : locale::LOG_CLAIM_OFFER_AGAINST,
|
|
formatEmpireName(emp, contactCheck=playerEmpire), offer.blurb, toString(offer.support), formatEmpireName(offer.fromEmpire, contactCheck=playerEmpire));
|
|
case IVET_Card: {
|
|
string text;
|
|
for(uint i = 0, cnt = cardEvent.card.type.hooks.length; i < cnt; ++i) {
|
|
if(cardEvent.card.type.hooks[i].formatEvent(cardEvent.card, this, text))
|
|
return text;
|
|
}
|
|
return text;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
bool visibleTo(Empire@ emp) const {
|
|
if(emp is null || !emp.valid)
|
|
return true;
|
|
return visionMask & emp.mask != 0;
|
|
}
|
|
|
|
bool isActiveEffect(const InfluenceVote@ vote = null) const {
|
|
if(cardEvent is null || cardEvent.card is null)
|
|
return false;
|
|
for(uint i = 0, cnt = cardEvent.card.type.hooks.length; i < cnt; ++i) {
|
|
if(cardEvent.card.type.hooks[i].isActiveEffect(cardEvent.card, this, vote))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
InfluenceCardSide get_playedSide() const {
|
|
if(type == IVET_Card) {
|
|
if(cardEvent.card.type.sideMode == ICS_Both) {
|
|
auto@ targ = cardEvent.targets[cardEvent.card.type.sideTarget];
|
|
if(targ is null) {
|
|
if(weight < 0)
|
|
return ICS_Oppose;
|
|
if(weight > 0)
|
|
return ICS_Support;
|
|
return ICS_Neutral;
|
|
}
|
|
else {
|
|
return targ.side ? ICS_Support : ICS_Oppose;
|
|
}
|
|
}
|
|
else {
|
|
return cardEvent.card.type.sideMode;
|
|
}
|
|
}
|
|
else {
|
|
if(weight < 0)
|
|
return ICS_Oppose;
|
|
if(weight > 0)
|
|
return ICS_Support;
|
|
return ICS_Neutral;
|
|
}
|
|
}
|
|
|
|
//Network
|
|
InfluenceVoteEvent(Message& msg) {
|
|
read(msg);
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
uint Type = type;
|
|
msg << Type;
|
|
msg << emp;
|
|
msg << text;
|
|
msg << time;
|
|
msg << eventType;
|
|
msg << weight;
|
|
msg << visionMask;
|
|
if(cardEvent !is null) {
|
|
msg.write1();
|
|
msg << cardEvent;
|
|
}
|
|
else {
|
|
msg.write0();
|
|
}
|
|
if(offer !is null) {
|
|
msg.write1();
|
|
msg << offer;
|
|
}
|
|
else {
|
|
msg.write0();
|
|
}
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
uint Type = 0;
|
|
msg >> Type;
|
|
type = InfluenceVoteEventType(Type);
|
|
msg >> emp;
|
|
msg >> text;
|
|
msg >> time;
|
|
msg >> eventType;
|
|
msg >> weight;
|
|
msg >> visionMask;
|
|
if(msg.readBit()) {
|
|
if(cardEvent !is null)
|
|
cardEvent.read(msg);
|
|
else
|
|
@cardEvent = InfluenceCardPlayEvent(msg);
|
|
}
|
|
if(msg.readBit()) {
|
|
if(offer is null)
|
|
@offer = InfluenceVoteOffer();
|
|
msg >> offer;
|
|
}
|
|
}
|
|
|
|
//Saving
|
|
InfluenceVoteEvent(SaveFile& file) {
|
|
load(file);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
uint Type = type;
|
|
file << Type;
|
|
file << emp;
|
|
file << text;
|
|
file << time;
|
|
file << eventType;
|
|
file << weight;
|
|
file << visionMask;
|
|
if(cardEvent !is null) {
|
|
file.write1();
|
|
file << cardEvent;
|
|
}
|
|
else {
|
|
file.write0();
|
|
}
|
|
if(offer !is null) {
|
|
file.write1();
|
|
file << offer;
|
|
}
|
|
else {
|
|
file.write0();
|
|
}
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
uint Type = 0;
|
|
file >> Type;
|
|
type = InfluenceVoteEventType(Type);
|
|
file >> emp;
|
|
file >> text;
|
|
file >> time;
|
|
file >> eventType;
|
|
file >> weight;
|
|
file >> visionMask;
|
|
|
|
bool loadCard = false;
|
|
if(file >= SV_0042)
|
|
loadCard = file.readBit();
|
|
else
|
|
loadCard = type == IVET_Card;
|
|
if(loadCard) {
|
|
if(cardEvent !is null)
|
|
cardEvent.load(file);
|
|
else
|
|
@cardEvent = InfluenceCardPlayEvent(file);
|
|
}
|
|
if(file >= SV_0055) {
|
|
if(file.readBit()) {
|
|
if(offer is null)
|
|
@offer = InfluenceVoteOffer();
|
|
file >> offer;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Initialization
|
|
array<InfluenceVoteType@> voteList;
|
|
dictionary voteIdents;
|
|
|
|
uint getInfluenceVoteTypeCount() {
|
|
return voteList.length;
|
|
}
|
|
|
|
const InfluenceVoteType@ getInfluenceVoteType(uint id) {
|
|
if(id >= voteList.length)
|
|
return null;
|
|
return voteList[id];
|
|
}
|
|
|
|
const InfluenceVoteType@ getInfluenceVoteType(const string& ident) {
|
|
InfluenceVoteType@ type;
|
|
voteIdents.get(ident, @type);
|
|
return type;
|
|
}
|
|
|
|
void addInfluenceVoteType(InfluenceVoteType@ type) {
|
|
type.id = voteList.length;
|
|
voteIdents.set(type.ident, @type);
|
|
voteList.insertLast(type);
|
|
}
|
|
|
|
int getInfluenceVoteID(const string& ident) {
|
|
InfluenceVoteType@ type;
|
|
voteIdents.get(ident, @type);
|
|
if(type is null)
|
|
return -1;
|
|
return type.id;
|
|
}
|
|
|
|
string getInfluenceVoteIdent(int id) {
|
|
if(uint(id) >= voteList.length)
|
|
return "";
|
|
return voteList[id].ident;
|
|
}
|
|
|
|
void readVoteLine(const string& line, InfluenceVoteType@ type, ReadFile@ file) {
|
|
auto@ hook = cast<InfluenceVoteEffect>(parseHook(line, "vote_effects::", instantiate=false, file=file));
|
|
if(hook !is null) {
|
|
hook.hookIndex = type.hooks.length;
|
|
type.hooks.insertLast(hook);
|
|
}
|
|
}
|
|
|
|
bool readVote(ReadFile@ file) {
|
|
InfluenceVoteType@ type;
|
|
do {
|
|
if(file.fullLine) {
|
|
if(type !is null)
|
|
readVoteLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else if(file.key == "Vote") {
|
|
if(type !is null)
|
|
addInfluenceVoteType(type);
|
|
@type = InfluenceVoteType();
|
|
type.ident = file.value;
|
|
}
|
|
else if(file.key == "Name") {
|
|
type.name = localize(file.value);
|
|
}
|
|
else if(file.key == "Description") {
|
|
type.description = localize(file.value);
|
|
}
|
|
else if(file.key == "Color") {
|
|
type.color = toColor(file.value);
|
|
}
|
|
else if(file.key == "Icon") {
|
|
type.icon = getSprite(file.value);
|
|
}
|
|
else if(file.key == "Target") {
|
|
parseTarget(type.targets, file.value);
|
|
}
|
|
else if(file.key == "AI") {
|
|
auto@ hook = parseHook(file.value, "ai.diplomacy::", instantiate=false, file=file);
|
|
if(hook !is null)
|
|
type.ai.insertLast(hook);
|
|
}
|
|
else {
|
|
if(file.line.findFirst("(") != -1) {
|
|
if(type !is null)
|
|
readVoteLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else {
|
|
if(type !is null)
|
|
addInfluenceVoteType(type);
|
|
return false;
|
|
}
|
|
}
|
|
} while(file++);
|
|
|
|
if(type !is null)
|
|
addInfluenceVoteType(type);
|
|
return true;
|
|
}
|
|
//}}}
|
|
// }}}
|
|
// {{{ Influence effects
|
|
export InfluenceEffectType;
|
|
export InfluenceEffect;
|
|
export InfluenceEffectEffect;
|
|
|
|
export getInfluenceEffectTypeCount;
|
|
export getInfluenceEffectType;
|
|
|
|
//{{{ Types
|
|
tidy final class InfluenceEffectType {
|
|
uint id = 0;
|
|
string ident;
|
|
string name;
|
|
string description;
|
|
Sprite icon;
|
|
Color color;
|
|
|
|
double reservation = 0.0;
|
|
double defaultDuration = -1.0;
|
|
bool dismissAble = true;
|
|
bool dismissNeedOwner = true;
|
|
Targets targets;
|
|
|
|
array<string> tags;
|
|
array<IInfluenceEffectEffect@> hooks;
|
|
|
|
void init() {
|
|
for(uint i = 0, cnt = hooks.length; i < cnt; ++i) {
|
|
auto@ hook = cast<Hook>(hooks[i]);
|
|
if(!hook.instantiate())
|
|
error("Could not instantiate hook: "+addrstr(hook)+" in "+ident);
|
|
hook.initTargets(targets);
|
|
hooks[i].init(this);
|
|
}
|
|
}
|
|
|
|
bool hasTag(const string& tag) const {
|
|
return tags.find(tag) != -1;
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Effects
|
|
tidy final class InfluenceEffect : Serializable, Savable {
|
|
const InfluenceEffectType@ type;
|
|
Targets targets;
|
|
|
|
int id = -1;
|
|
int reserveId = -1;
|
|
array<any> data;
|
|
Empire@ owner;
|
|
|
|
double remainingTime = -1.0;
|
|
bool active = false;
|
|
|
|
bool delta = false;
|
|
|
|
InfluenceEffect() {
|
|
}
|
|
|
|
InfluenceEffect(const InfluenceEffectType@ Type) {
|
|
set(Type);
|
|
}
|
|
|
|
void set(const InfluenceEffectType@ Type) {
|
|
@type = Type;
|
|
targets.set(type.targets);
|
|
data.length = type.hooks.length;
|
|
}
|
|
|
|
bool checkTargets(const Targets@ check) const {
|
|
if(check is null)
|
|
return true;
|
|
if(check.targets.length != type.targets.targets.length)
|
|
return false;
|
|
for(uint i = 0, cnt = check.targets.length; i < cnt; ++i) {
|
|
if(!check.targets[i].filled)
|
|
return false;
|
|
if(check.targets[i].type != type.targets.targets[i].type)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool canDismiss(Empire@ byEmpire) const {
|
|
if(!type.dismissAble)
|
|
return false;
|
|
if(type.dismissNeedOwner) {
|
|
if(byEmpire !is owner)
|
|
return false;
|
|
}
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(!type.hooks[i].canDismiss(this, byEmpire))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#section server
|
|
void start(Targets@ targets, Empire@ forEmpire, double duration = 0.0) {
|
|
@owner = forEmpire;
|
|
targets = targets;
|
|
if(duration == 0)
|
|
remainingTime = type.defaultDuration;
|
|
else
|
|
remainingTime = duration;
|
|
active = true;
|
|
delta = true;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onStart(this);
|
|
if(type.reservation != 0.0 && owner !is null)
|
|
reserveId = owner.reserveInfluence(type.reservation);
|
|
}
|
|
|
|
void tick(double time) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i) {
|
|
if(type.hooks[i].onTick(this, time))
|
|
delta = true;
|
|
}
|
|
|
|
if(active && remainingTime >= 0) {
|
|
remainingTime -= time;
|
|
if(remainingTime < 0) {
|
|
remainingTime = 0.0;
|
|
end();
|
|
}
|
|
}
|
|
}
|
|
|
|
void dismiss(Empire@ byEmpire) {
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onDismiss(this, byEmpire);
|
|
end();
|
|
}
|
|
|
|
void end() {
|
|
active = false;
|
|
delta = true;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].onEnd(this);
|
|
if(reserveId != -1 && owner !is null)
|
|
owner.removeInfluenceReservation(reserveId);
|
|
}
|
|
#section shadow
|
|
void tick(double time) {
|
|
if(remainingTime >= 0)
|
|
remainingTime -= time;
|
|
}
|
|
#section all
|
|
|
|
//Networking
|
|
InfluenceEffect(Message& msg) {
|
|
read(msg);
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg << type.id;
|
|
targets.writeData(msg, type.targets);
|
|
msg << id;
|
|
msg << owner;
|
|
msg << remainingTime;
|
|
msg << active;
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
uint typeId = 0;
|
|
msg >> typeId;
|
|
@type = getInfluenceEffectType(typeId);
|
|
targets.readData(msg, type.targets);
|
|
data.length = type.hooks.length;
|
|
msg >> id;
|
|
msg >> owner;
|
|
msg >> remainingTime;
|
|
msg >> active;
|
|
}
|
|
|
|
//Saving
|
|
InfluenceEffect(SaveFile& file) {
|
|
load(file);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file.writeIdentifier(SI_InfluenceEffect, type.id);
|
|
targets.saveData(file, type.targets);
|
|
file << id;
|
|
file << owner;
|
|
file << reserveId;
|
|
file << remainingTime;
|
|
file << active;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].save(this, file);
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
uint typeId = 0;
|
|
if(file == SV_0063)
|
|
file >> typeId;
|
|
else
|
|
typeId = file.readIdentifier(SI_InfluenceEffect);
|
|
|
|
@type = getInfluenceEffectType(typeId);
|
|
data.length = type.hooks.length;
|
|
targets.loadData(file, type.targets);
|
|
file >> id;
|
|
file >> owner;
|
|
file >> reserveId;
|
|
file >> remainingTime;
|
|
file >> active;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].load(this, file);
|
|
}
|
|
|
|
//Formatting
|
|
string formatTitle() const {
|
|
return targets.format(type.name);
|
|
}
|
|
|
|
string formatDescription() const {
|
|
return targets.format(type.description);
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Effects
|
|
interface IInfluenceEffectEffect {
|
|
void set_dataIndex(uint ind);
|
|
|
|
void init(InfluenceEffectType@ type);
|
|
void onStart(InfluenceEffect@ effect) const;
|
|
bool onTick(InfluenceEffect@ effect, double time) const;
|
|
void onDismiss(InfluenceEffect@ effect, Empire@ byEmpire) const;
|
|
void onEnd(InfluenceEffect@ effect) const;
|
|
bool canDismiss(const InfluenceEffect@ effect, Empire@ byEmpire) const;
|
|
void save(InfluenceEffect@ effect, SaveFile& file) const;
|
|
void load(InfluenceEffect@ effect, SaveFile& file) const;
|
|
};
|
|
|
|
class InfluenceEffectEffect : Hook, IInfluenceEffectEffect {
|
|
uint hookIndex = 0;
|
|
|
|
void set_dataIndex(uint ind) {
|
|
hookIndex = ind;
|
|
}
|
|
|
|
void init(InfluenceEffectType@ type) {
|
|
}
|
|
|
|
void onStart(InfluenceEffect@ effect) const {
|
|
}
|
|
|
|
bool onTick(InfluenceEffect@ effect, double time) const {
|
|
return false;
|
|
}
|
|
|
|
void onDismiss(InfluenceEffect@ effect, Empire@ byEmpire) const {
|
|
}
|
|
|
|
void onEnd(InfluenceEffect@ effect) const {
|
|
}
|
|
|
|
bool canDismiss(const InfluenceEffect@ effect, Empire@ byEmpire) const {
|
|
return true;
|
|
}
|
|
|
|
void save(InfluenceEffect@ effect, SaveFile& file) const {
|
|
}
|
|
|
|
void load(InfluenceEffect@ effect, SaveFile& file) const {
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Initialization
|
|
array<InfluenceEffectType@> effectList;
|
|
dictionary effectIdents;
|
|
|
|
uint getInfluenceEffectTypeCount() {
|
|
return effectList.length;
|
|
}
|
|
|
|
const InfluenceEffectType@ getInfluenceEffectType(uint id) {
|
|
if(id >= effectList.length)
|
|
return null;
|
|
return effectList[id];
|
|
}
|
|
|
|
const InfluenceEffectType@ getInfluenceEffectType(const string& ident) {
|
|
InfluenceEffectType@ type;
|
|
effectIdents.get(ident, @type);
|
|
return type;
|
|
}
|
|
|
|
int getInfluenceEffectID(const string& ident) {
|
|
InfluenceEffectType@ type;
|
|
effectIdents.get(ident, @type);
|
|
if(type is null)
|
|
return -1;
|
|
return type.id;
|
|
}
|
|
|
|
string getInfluenceEffectIdent(int id) {
|
|
if(uint(id) >= effectList.length)
|
|
return "";
|
|
return effectList[id].ident;
|
|
}
|
|
|
|
void addInfluenceEffectType(InfluenceEffectType@ type) {
|
|
type.id = effectList.length;
|
|
effectIdents.set(type.ident, @type);
|
|
effectList.insertLast(type);
|
|
}
|
|
|
|
void readEffectLine(const string& line, InfluenceEffectType@ type, ReadFile@ file) {
|
|
auto@ hook = cast<IInfluenceEffectEffect>(parseHook(line, "influence_effects::", instantiate=false, file=file));
|
|
if(hook !is null) {
|
|
hook.dataIndex = type.hooks.length;
|
|
type.hooks.insertLast(hook);
|
|
}
|
|
}
|
|
|
|
bool readEffect(ReadFile@ file) {
|
|
InfluenceEffectType@ type;
|
|
do {
|
|
if(file.fullLine) {
|
|
if(type !is null)
|
|
readEffectLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else if(file.key == "Effect") {
|
|
if(type !is null)
|
|
addInfluenceEffectType(type);
|
|
@type = InfluenceEffectType();
|
|
type.ident = file.value;
|
|
}
|
|
else if(file.key == "Name") {
|
|
type.name = localize(file.value);
|
|
}
|
|
else if(file.key == "Description") {
|
|
type.description = localize(file.value);
|
|
}
|
|
else if(file.key == "Color") {
|
|
type.color = toColor(file.value);
|
|
}
|
|
else if(file.key == "Icon") {
|
|
type.icon = getSprite(file.value);
|
|
}
|
|
else if(file.key == "Target") {
|
|
parseTarget(type.targets, file.value);
|
|
}
|
|
else if(file.key == "Default Duration") {
|
|
type.defaultDuration = toDouble(file.value);
|
|
}
|
|
else if(file.key == "Upkeep") {
|
|
type.reservation = toDouble(file.value);
|
|
}
|
|
else if(file.key == "Tag") {
|
|
type.tags.insertLast(file.value);
|
|
}
|
|
else if(file.key == "Dismissable") {
|
|
type.dismissAble = toBool(file.value);
|
|
}
|
|
else if(file.key == "Dismiss Needs Owner") {
|
|
type.dismissNeedOwner = toBool(file.value);
|
|
}
|
|
else {
|
|
if(file.line.findFirst("(") != -1) {
|
|
if(type !is null)
|
|
readEffectLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else {
|
|
if(type !is null)
|
|
addInfluenceEffectType(type);
|
|
return false;
|
|
}
|
|
}
|
|
} while(file++);
|
|
|
|
if(type !is null)
|
|
addInfluenceEffectType(type);
|
|
return true;
|
|
}
|
|
//}}}
|
|
// }}}
|
|
// {{{ Treaties
|
|
export InfluenceClauseType;
|
|
export getInfluenceClauseType, getInfluenceClauseTypeCount;
|
|
|
|
export Treaty, Clause;
|
|
|
|
//{{{ Clause Types
|
|
tidy final class InfluenceClauseType {
|
|
uint id = 0;
|
|
string ident;
|
|
string name;
|
|
string description;
|
|
Sprite icon;
|
|
Color color;
|
|
|
|
bool freeClause = false;
|
|
bool teamClause = false;
|
|
bool defaultClause = false;
|
|
array<InfluenceClauseHook@> hooks;
|
|
|
|
void init() {
|
|
for(uint i = 0, cnt = hooks.length; i < cnt; ++i) {
|
|
if(!hooks[i].instantiate())
|
|
error("Could not instantiate hook: "+addrstr(hooks[i])+" in "+ident);
|
|
}
|
|
}
|
|
};
|
|
|
|
class InfluenceClauseHook : Hook {
|
|
uint hookIndex = 0;
|
|
|
|
void onStart(Treaty@ treaty, Clause@ clause) const {}
|
|
void onTick(Treaty@ treaty, Clause@ clause, double time) const {}
|
|
void onEnd(Treaty@ treaty, Clause@ clause) const {}
|
|
|
|
void onJoin(Treaty@ treaty, Clause@ clause, Empire@ joined) const {}
|
|
void onLeave(Treaty@ treaty, Clause@ clause, Empire@ left) const {}
|
|
|
|
bool isVisibleTo(const Treaty@ treaty, const Clause@ clause, Empire& empire) const { return true; }
|
|
bool canLeave(const Treaty@ treaty, const Clause@ clause, Empire& empire) const { return true; }
|
|
bool canInvite(const Treaty@ treaty, const Clause@ clause, Empire& from, Empire& invite) const { return true; }
|
|
|
|
void save(Clause@ clause, SaveFile& file) const {}
|
|
void load(Clause@ clause, SaveFile& file) const {}
|
|
};
|
|
//}}}
|
|
//{{{ Treaties
|
|
enum TreatyEventType {
|
|
TET_Invite,
|
|
TET_Leave,
|
|
TET_Dismiss,
|
|
TET_Join,
|
|
TET_Decline,
|
|
TET_Subjugate
|
|
};
|
|
|
|
tidy final class Treaty : Serializable, Savable {
|
|
uint id = 0;
|
|
string name;
|
|
|
|
array<Clause@> clauses;
|
|
|
|
uint visibleMask = uint(~0);
|
|
uint inviteMask = 0;
|
|
uint presentMask = 0;
|
|
|
|
Empire@ leader;
|
|
array<Empire@> joinedEmpires;
|
|
bool started = false;
|
|
bool delta = false;
|
|
|
|
void write(Message& msg) {
|
|
msg.writeSmall(id);
|
|
msg << name;
|
|
msg.writeSmall(clauses.length);
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i)
|
|
msg << clauses[i];
|
|
msg.writeSignedSmall(visibleMask);
|
|
msg.writeSmall(inviteMask);
|
|
msg.writeSmall(presentMask);
|
|
msg << leader;
|
|
msg.writeSmall(joinedEmpires.length);
|
|
for(uint i = 0, cnt = joinedEmpires.length; i < cnt; ++i)
|
|
msg << joinedEmpires[i];
|
|
msg << started;
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
id = msg.readSmall();
|
|
msg >> name;
|
|
clauses.length = msg.readSmall();
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
if(clauses[i] is null)
|
|
@clauses[i] = Clause();
|
|
msg >> clauses[i];
|
|
}
|
|
visibleMask = msg.readSignedSmall();
|
|
inviteMask = msg.readSmall();
|
|
presentMask = msg.readSmall();
|
|
msg >> leader;
|
|
joinedEmpires.length = msg.readSmall();
|
|
for(uint i = 0, cnt = joinedEmpires.length; i < cnt; ++i)
|
|
msg >> joinedEmpires[i];
|
|
msg >> started;
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file << id;
|
|
file << name;
|
|
file << clauses.length;
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i)
|
|
file << clauses[i];
|
|
file << visibleMask << inviteMask << presentMask;
|
|
file << leader;
|
|
file << joinedEmpires.length;
|
|
for(uint i = 0, cnt = joinedEmpires.length; i < cnt; ++i)
|
|
file << joinedEmpires[i];
|
|
file << started;
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
file >> id;
|
|
file >> name;
|
|
|
|
uint cnt = 0;
|
|
file >> cnt;
|
|
clauses.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
if(clauses[i] is null)
|
|
@clauses[i] = Clause();
|
|
file >> clauses[i];
|
|
}
|
|
|
|
file >> visibleMask >> inviteMask >> presentMask;
|
|
file >> leader;
|
|
|
|
file >> cnt;
|
|
joinedEmpires.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
file >> joinedEmpires[i];
|
|
file >> started;
|
|
}
|
|
|
|
bool canLeave(Empire& emp) const {
|
|
if(joinedEmpires.length == 1 && presentMask & emp.mask != 0)
|
|
return true;
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
if(!clause.type.hooks[n].canLeave(this, clause, emp))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool canInvite(Empire& emp, Empire& invite) const {
|
|
if(leader !is null && leader !is emp)
|
|
return false;
|
|
if(presentMask != 0 && presentMask & emp.mask == 0)
|
|
return false;
|
|
if(inviteMask & invite.mask != 0)
|
|
return false;
|
|
if(presentMask & invite.mask != 0)
|
|
return false;
|
|
if(emp.SubjugatedBy !is null)
|
|
return false;
|
|
if(invite.SubjugatedBy !is null && (invite.SubjugatedBy !is leader || emp !is leader))
|
|
return false;
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
if(!clause.type.hooks[n].canInvite(this, clause, emp, invite))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool isVisibleTo(Empire@ emp) const {
|
|
if(emp is spectatorEmpire)
|
|
return true;
|
|
if(emp is null)
|
|
return false;
|
|
if(visibleMask & emp.mask == 0)
|
|
return false;
|
|
if(inviteMask & emp.mask != 0 || presentMask & emp.mask != 0)
|
|
return true;
|
|
if(leader !is null) {
|
|
if(emp.ContactMask & leader.mask == 0)
|
|
return false;
|
|
if(joinedEmpires.length <= 1)
|
|
return false;
|
|
}
|
|
else {
|
|
if(joinedEmpires.length <= 1)
|
|
return false;
|
|
for(uint i = 0, cnt = joinedEmpires.length; i < cnt; ++i) {
|
|
if(emp.ContactMask & joinedEmpires[i].mask == 0)
|
|
return false;
|
|
}
|
|
}
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
if(!clause.type.hooks[n].isVisibleTo(this, clause, emp))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool hasClause(const string& name) const {
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
if(clauses[i].type.ident == name)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool hasClause(const InfluenceClauseType@ type) const {
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
if(clauses[i].type is type)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#section client
|
|
void addClause(const InfluenceClauseType@ type) {
|
|
Clause clause(type);
|
|
clauses.insertLast(clause);
|
|
}
|
|
|
|
string getTooltip() const {
|
|
string tt;
|
|
if(leader !is null)
|
|
tt += format("[font=Medium]$1's [b]$2[/b][/font]",
|
|
formatEmpireName(leader), name);
|
|
else
|
|
tt += format("[font=Medium][b]$1[/b][/font]", name);
|
|
tt += "\n"+locale::SIGNED_BY+" ";
|
|
for(uint i = 0, cnt = joinedEmpires.length; i < cnt; ++i) {
|
|
if(i != 0)
|
|
tt += ", ";
|
|
tt += formatEmpireName(joinedEmpires[i]);
|
|
}
|
|
if(inviteMask != 0) {
|
|
tt += "\n\n[b]"+locale::INVITED_TO+":\n[offset=30]";
|
|
bool haveInvites = false;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
auto@ other = getEmpire(i);
|
|
if(other.mask & inviteMask != 0) {
|
|
if(haveInvites)
|
|
tt += ", ";
|
|
tt += formatEmpireName(other);
|
|
haveInvites = true;
|
|
}
|
|
}
|
|
tt += "[/offset][/b]";
|
|
}
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
tt += format("\n\n[b][img=$3;24/] $1[/b]\n$2",
|
|
clauses[i].type.name, clauses[i].type.description,
|
|
getSpriteDesc(clauses[i].type.icon));
|
|
}
|
|
return tt;
|
|
}
|
|
#section server
|
|
void init(Empire& starter) {
|
|
if(leader !is null)
|
|
@leader = starter;
|
|
presentMask = starter.mask;
|
|
visibleMask = uint(~0);
|
|
started = false;
|
|
delta = true;
|
|
}
|
|
|
|
void invite(Empire& inviter, Empire& emp) {
|
|
if(presentMask & emp.mask == 0) {
|
|
inviteMask |= emp.mask;
|
|
visibleMask |= emp.mask;
|
|
delta = true;
|
|
|
|
//Send invite to empire
|
|
emp.notifyTreaty(id, TET_Invite, inviter, emp);
|
|
|
|
//Send invite notification to other members
|
|
for(uint i = 0, cnt = joinedEmpires.length; i < cnt; ++i) {
|
|
auto@ other = joinedEmpires[i];
|
|
if(other is inviter || other is emp)
|
|
continue;
|
|
other.notifyTreaty(id, TET_Invite, emp);
|
|
}
|
|
}
|
|
}
|
|
|
|
void join(Empire& emp, bool force = false) {
|
|
if(!force && inviteMask & emp.mask == 0)
|
|
return;
|
|
delta = true;
|
|
inviteMask &= ~emp.mask;
|
|
presentMask |= emp.mask;
|
|
joinedEmpires.insertLast(emp);
|
|
if(started) {
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
clause.type.hooks[n].onJoin(this, clause, emp);
|
|
}
|
|
}
|
|
}
|
|
else if(joinedEmpires.length == 2) {
|
|
start();
|
|
}
|
|
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
auto@ other = getEmpire(i);
|
|
if(!other.major || !isVisibleTo(other))
|
|
continue;
|
|
if(other is emp)
|
|
continue;
|
|
other.notifyTreaty(id, TET_Join, emp);
|
|
}
|
|
}
|
|
|
|
void leave(Empire& emp) {
|
|
delta = true;
|
|
inviteMask &= ~emp.mask;
|
|
if(joinedEmpires.find(emp) != -1) {
|
|
presentMask &= ~emp.mask;
|
|
joinedEmpires.remove(emp);
|
|
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
|
|
auto@ other = getEmpire(i);
|
|
if(!other.major || !isVisibleTo(other))
|
|
continue;
|
|
if(other is emp)
|
|
continue;
|
|
other.notifyTreaty(id, emp is leader ? TET_Dismiss : TET_Leave, emp);
|
|
}
|
|
|
|
if(started) {
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
clause.type.hooks[n].onLeave(this, clause, emp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void decline(Empire& emp) {
|
|
delta = true;
|
|
inviteMask &= ~emp.mask;
|
|
|
|
//Send decline message to members
|
|
for(uint i = 0, cnt = joinedEmpires.length; i < cnt; ++i) {
|
|
auto@ other = joinedEmpires[i];
|
|
if(other is emp)
|
|
continue;
|
|
other.notifyTreaty(id, TET_Decline, emp);
|
|
}
|
|
}
|
|
|
|
void start() {
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
clause.type.hooks[n].onStart(this, clause);
|
|
}
|
|
}
|
|
started = true;
|
|
delta = true;
|
|
}
|
|
|
|
void tick(double time) {
|
|
if(started) {
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
clause.type.hooks[n].onTick(this, clause, time);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void end() {
|
|
if(started) {
|
|
for(uint i = 0, cnt = clauses.length; i < cnt; ++i) {
|
|
auto@ clause = clauses[i];
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n) {
|
|
clause.type.hooks[n].onEnd(this, clause);
|
|
}
|
|
}
|
|
}
|
|
started = false;
|
|
delta = true;
|
|
}
|
|
|
|
void addClause(const InfluenceClauseType@ type) {
|
|
if(type is null)
|
|
return;
|
|
|
|
auto@ clause = Clause(type);
|
|
if(started) {
|
|
for(uint n = 0, ncnt = clause.type.hooks.length; n < ncnt; ++n)
|
|
clause.type.hooks[n].onStart(this, clause);
|
|
}
|
|
clauses.insertLast(clause);
|
|
delta = true;
|
|
}
|
|
#section all
|
|
};
|
|
|
|
tidy final class Clause : Serializable, Savable {
|
|
const InfluenceClauseType@ type;
|
|
array<any> data;
|
|
|
|
Clause() {}
|
|
Clause(const InfluenceClauseType@ type) { set(type); }
|
|
void set(const InfluenceClauseType@ type) {
|
|
@this.type = type;
|
|
data.length = type.hooks.length;
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg.writeSmall(type.id);
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
@type = getInfluenceClauseType(msg.readSmall());
|
|
data.length = type.hooks.length;
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file.writeIdentifier(SI_InfluenceClause, type.id);
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].save(this, file);
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
@type = getInfluenceClauseType(file.readIdentifier(SI_InfluenceClause));
|
|
data.length = type.hooks.length;
|
|
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
|
|
type.hooks[i].load(this, file);
|
|
}
|
|
};
|
|
//}}}
|
|
//{{{ Initialization
|
|
array<InfluenceClauseType@> clauseList;
|
|
dictionary clauseIdents;
|
|
|
|
uint getInfluenceClauseTypeCount() {
|
|
return clauseList.length;
|
|
}
|
|
|
|
const InfluenceClauseType@ getInfluenceClauseType(uint id) {
|
|
if(id >= clauseList.length)
|
|
return null;
|
|
return clauseList[id];
|
|
}
|
|
|
|
const InfluenceClauseType@ getInfluenceClauseType(const string& ident) {
|
|
InfluenceClauseType@ type;
|
|
clauseIdents.get(ident, @type);
|
|
return type;
|
|
}
|
|
|
|
void addInfluenceClauseType(InfluenceClauseType@ type) {
|
|
type.id = clauseList.length;
|
|
clauseIdents.set(type.ident, @type);
|
|
clauseList.insertLast(type);
|
|
}
|
|
|
|
void readClauseLine(const string& line, InfluenceClauseType@ type, ReadFile@ file) {
|
|
auto@ hook = cast<InfluenceClauseHook>(parseHook(line, "clause_effects::", instantiate=false, file=file));
|
|
if(hook !is null) {
|
|
hook.hookIndex = type.hooks.length;
|
|
type.hooks.insertLast(hook);
|
|
}
|
|
}
|
|
|
|
bool readClause(ReadFile@ file) {
|
|
InfluenceClauseType@ type;
|
|
do {
|
|
if(file.fullLine) {
|
|
if(type !is null)
|
|
readClauseLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else if(file.key == "Clause") {
|
|
if(type !is null)
|
|
addInfluenceClauseType(type);
|
|
@type = InfluenceClauseType();
|
|
type.ident = file.value;
|
|
}
|
|
else if(file.key == "Name") {
|
|
type.name = localize(file.value);
|
|
}
|
|
else if(file.key == "Description") {
|
|
type.description = localize(file.value);
|
|
}
|
|
else if(file.key == "Color") {
|
|
type.color = toColor(file.value);
|
|
}
|
|
else if(file.key == "Icon") {
|
|
type.icon = getSprite(file.value);
|
|
}
|
|
else if(file.key == "Free Clause") {
|
|
type.freeClause = toBool(file.value);
|
|
}
|
|
else if(file.key == "Team Clause") {
|
|
type.teamClause = toBool(file.value);
|
|
}
|
|
else if(file.key == "Default Clause") {
|
|
type.defaultClause = toBool(file.value);
|
|
}
|
|
else {
|
|
if(file.line.findFirst("(") != -1) {
|
|
if(type !is null)
|
|
readClauseLine(file.line, type, file);
|
|
else
|
|
file.error("Unexpected line: "+escape(file.line));
|
|
}
|
|
else {
|
|
if(type !is null)
|
|
addInfluenceClauseType(type);
|
|
return false;
|
|
}
|
|
}
|
|
} while(file++);
|
|
|
|
if(type !is null)
|
|
addInfluenceClauseType(type);
|
|
return true;
|
|
}
|
|
//}}}
|
|
// }}}
|
|
// {{{ Offers
|
|
export DiplomacyOfferType, DiplomacyOffer;
|
|
export InfluenceVoteOffer;
|
|
|
|
enum DiplomacyOfferType {
|
|
DOT_Money,
|
|
DOT_Energy,
|
|
DOT_Card,
|
|
DOT_Fleet,
|
|
DOT_Planet,
|
|
DOT_Artifact,
|
|
|
|
DOT_COUNT,
|
|
DOT_INVALID = DOT_COUNT
|
|
};
|
|
|
|
tidy final class DiplomacyOffer : Serializable, Savable {
|
|
Empire@ bound;
|
|
DiplomacyOfferType type = DOT_INVALID;
|
|
double value = 0;
|
|
int id = -1;
|
|
Object@ obj;
|
|
InfluenceCard@ memoCard;
|
|
|
|
DiplomacyOffer() {
|
|
}
|
|
|
|
DiplomacyOffer(DiplomacyOfferType type, Object@ obj) {
|
|
this.type = type;
|
|
@this.obj = obj;
|
|
}
|
|
|
|
DiplomacyOffer(DiplomacyOfferType type, double value = 0.0, int id = 0) {
|
|
this.type = type;
|
|
this.value = value;
|
|
this.id = id;
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file << uint(type);
|
|
file << bound;
|
|
file << value;
|
|
file << obj;
|
|
file << id;
|
|
if(memoCard !is null) {
|
|
file.write1();
|
|
file << memoCard;
|
|
}
|
|
else {
|
|
file.write0();
|
|
}
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
uint tp = 0;
|
|
file >> tp;
|
|
type = DiplomacyOfferType(tp);
|
|
file >> bound;
|
|
file >> value;
|
|
file >> obj;
|
|
file >> id;
|
|
if(file.readBit()) {
|
|
if(memoCard is null)
|
|
@memoCard = InfluenceCard();
|
|
file >> memoCard;
|
|
}
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg.writeSmall(type);
|
|
msg << bound;
|
|
msg << value;
|
|
msg << obj;
|
|
msg << id;
|
|
if(memoCard !is null) {
|
|
msg.write1();
|
|
msg << memoCard;
|
|
}
|
|
else {
|
|
msg.write0();
|
|
}
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
type = DiplomacyOfferType(msg.readSmall());
|
|
msg >> bound;
|
|
msg >> value;
|
|
msg >> obj;
|
|
msg >> id;
|
|
if(msg.readBit()) {
|
|
if(memoCard is null)
|
|
@memoCard = InfluenceCard();
|
|
msg >> memoCard;
|
|
}
|
|
}
|
|
|
|
void memo(Empire@ from) {
|
|
if(type == DOT_Card) {
|
|
@memoCard = InfluenceCard();
|
|
receive(from.getInfluenceCard(id), memoCard);
|
|
}
|
|
}
|
|
|
|
string get_blurb() const {
|
|
switch(type) {
|
|
case DOT_Money:
|
|
{
|
|
return format("[img=$3;20/] [b][color=$1]$2[/color][/b]",
|
|
toString(colors::Money), formatMoney(value),
|
|
getSpriteDesc(icons::Money));
|
|
}
|
|
case DOT_Energy:
|
|
{
|
|
return format("[img=$3;20/] [b][color=$1]$2[/color][/b]",
|
|
toString(colors::Energy), standardize(value, true),
|
|
getSpriteDesc(icons::Energy));
|
|
}
|
|
case DOT_Planet:
|
|
case DOT_Fleet:
|
|
case DOT_Artifact:
|
|
{
|
|
return format("[b]$1[/b]", formatObject(obj, showIcon=true));
|
|
}
|
|
case DOT_Card:
|
|
{
|
|
if(memoCard !is null) {
|
|
return memoCard.formatBlurb(int(value));
|
|
}
|
|
else if(bound !is null) {
|
|
InfluenceCard card;
|
|
if(receive(bound.getInfluenceCard(id), card))
|
|
return card.formatBlurb(int(value));
|
|
}
|
|
return "---";
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
bool canOffer(Empire@ from) {
|
|
switch(type) {
|
|
case DOT_Fleet: {
|
|
Ship@ ship = cast<Ship>(obj);
|
|
if(ship is null || !ship.valid)
|
|
return false;
|
|
if(!ship.hasLeaderAI || ship.owner !is from)
|
|
return false;
|
|
if(ship.blueprint.design.hasTag(ST_CannotDonate))
|
|
return false;
|
|
return true;
|
|
}
|
|
case DOT_Planet:
|
|
return obj !is null && obj.valid && obj.isPlanet && obj.owner is from;
|
|
case DOT_Money:
|
|
return from.canPay(value) && value <= INT_MAX/2 && value > 0;
|
|
case DOT_Energy:
|
|
return from.EnergyStored >= value && value > 0;
|
|
case DOT_Card:
|
|
return from.getInfluenceCardUses(id) >= int(value) && value > 0;
|
|
case DOT_Artifact:
|
|
{
|
|
if(obj is null || !obj.isArtifact)
|
|
return false;
|
|
Region@ region = obj.region;
|
|
return obj.valid && obj.owner !is null
|
|
&& (!obj.owner.valid || obj.owner is from)
|
|
&& region !is null && region.TradeMask & from.mask != 0;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool conflicts(Empire@ from, const DiplomacyOffer& other) const {
|
|
if(type == DOT_Card && id == other.id)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
#section server
|
|
bool take(Empire@ from) {
|
|
switch(type) {
|
|
case DOT_Money:
|
|
{
|
|
id = from.consumeBudget(value);
|
|
return id != -1;
|
|
}
|
|
case DOT_Energy:
|
|
{
|
|
double eng = from.consumeEnergy(value, consumePartial=false);
|
|
if(eng >= value - 0.001) {
|
|
from.modEnergyAllocated(value);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
case DOT_Card:
|
|
{
|
|
@bound = from;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void reverse(Empire@ from, bool initial = false) {
|
|
switch(type) {
|
|
case DOT_Money:
|
|
from.refundBudget(value, id);
|
|
break;
|
|
case DOT_Energy:
|
|
from.modEnergyStored(value);
|
|
from.modEnergyAllocated(-value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool valid(Empire@ from) {
|
|
switch(type) {
|
|
case DOT_Fleet: {
|
|
Ship@ ship = cast<Ship>(obj);
|
|
if(ship is null || !ship.valid)
|
|
return false;
|
|
if(!ship.hasLeaderAI || ship.owner !is from)
|
|
return false;
|
|
if(ship.blueprint.design.hasTag(ST_CannotDonate))
|
|
return false;
|
|
return true;
|
|
}
|
|
case DOT_Planet:
|
|
return obj !is null && obj.valid && obj.isPlanet && obj.owner is from;
|
|
case DOT_Card:
|
|
return from.getInfluenceCardUses(id) >= int(value);
|
|
case DOT_Artifact:
|
|
{
|
|
if(obj is null || !obj.isArtifact)
|
|
return false;
|
|
Region@ region = obj.region;
|
|
return obj.valid && obj.owner !is null
|
|
&& (!obj.owner.valid || obj.owner is from)
|
|
&& region !is null && region.TradeMask & from.mask != 0;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void delayMaintCost(Empire@ from, Empire@ to, int amount) {
|
|
if(amount <= 0)
|
|
return;
|
|
from.modForwardBudget(-amount);
|
|
to.modForwardBudget(amount);
|
|
}
|
|
|
|
bool give(Empire@ from, Empire@ to, bool delayMaintenance = false, bool preventFloat = false) {
|
|
switch(type) {
|
|
case DOT_Money:
|
|
{
|
|
if(preventFloat)
|
|
to.modRemainingBudget(value);
|
|
else
|
|
to.addBonusBudget(value);
|
|
return true;
|
|
}
|
|
case DOT_Energy:
|
|
{
|
|
double amt = value * to.EnergyEfficiency;
|
|
to.modEnergyStored(amt);
|
|
if(preventFloat) {
|
|
to.addFloatedEnergy(from, amt);
|
|
from.modEnergyAllocated(amt-value);
|
|
}
|
|
else
|
|
from.modEnergyAllocated(-value);
|
|
return true;
|
|
}
|
|
case DOT_Planet:
|
|
{
|
|
if(delayMaintenance)
|
|
delayMaintCost(from, to, -obj.income);
|
|
obj.takeoverPlanet(to);
|
|
return true;
|
|
}
|
|
case DOT_Fleet:
|
|
{
|
|
if(delayMaintenance)
|
|
delayMaintCost(from, to, cast<Ship>(obj).maintenanceCost);
|
|
obj.takeoverFleet(to, moveToTerritory=true);
|
|
return true;
|
|
}
|
|
case DOT_Card:
|
|
{
|
|
from.copyCardTo(id, to, int(value));
|
|
from.takeCardUse(id, int(value));
|
|
return true;
|
|
}
|
|
case DOT_Artifact:
|
|
{
|
|
if(obj.owner is from)
|
|
@obj.owner = to;
|
|
const SystemDesc@ targSys = getClosestSystem(obj.position, to);
|
|
if(targSys !is null) {
|
|
vec3d pos = targSys.position;
|
|
vec2d offset = random2d(150.0, targSys.radius - 150.0);
|
|
pos.x += offset.x;
|
|
pos.z += offset.y;
|
|
obj.orbitAround(pos, targSys.position);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
#section all
|
|
};
|
|
|
|
tidy final class InfluenceVoteOffer : Serializable, Savable {
|
|
int id = 0;
|
|
Empire@ fromEmpire;
|
|
bool side = true;
|
|
array<DiplomacyOffer> offers;
|
|
array<int> claims(getEmpireCount(), 0);
|
|
int support;
|
|
|
|
string get_blurb() const {
|
|
string text;
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(i != 0)
|
|
text += ", ";
|
|
text += offers[i].blurb;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
bool canOffer() {
|
|
if(offers.length == 0)
|
|
return false;
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(!offers[i].canOffer(fromEmpire))
|
|
return false;
|
|
for(uint j = 0; j < cnt; ++j) {
|
|
if(i == j)
|
|
continue;
|
|
if(offers[i].conflicts(fromEmpire, offers[j]))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#section server
|
|
bool take() {
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(!offers[i].take(fromEmpire)) {
|
|
for(uint j = 0; j < i; ++j)
|
|
offers[j].reverse(fromEmpire, initial=true);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void give(Empire@ emp) {
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i)
|
|
offers[i].give(fromEmpire, emp);
|
|
}
|
|
|
|
void memo() {
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i)
|
|
offers[i].memo(fromEmpire);
|
|
}
|
|
|
|
bool get_valid() {
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(!offers[i].valid(fromEmpire))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void invalidate() {
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i) {
|
|
if(offers[i].valid(fromEmpire))
|
|
offers[i].reverse(fromEmpire);
|
|
}
|
|
}
|
|
|
|
void expire() {
|
|
for(uint i = 0, cnt = offers.length; i < cnt; ++i)
|
|
offers[i].reverse(fromEmpire);
|
|
}
|
|
#section all
|
|
|
|
void save(SaveFile& file) {
|
|
file << id;
|
|
file << fromEmpire;
|
|
file << side;
|
|
file << support;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i)
|
|
file << claims[i];
|
|
uint cnt = offers.length;
|
|
file << cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
file << offers[i];
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
file >> id;
|
|
file >> fromEmpire;
|
|
file >> side;
|
|
file >> support;
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i)
|
|
file >> claims[i];
|
|
uint cnt = 0;
|
|
file >> cnt;
|
|
offers.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
file >> offers[i];
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg.writeSmall(id);
|
|
msg << fromEmpire;
|
|
msg << side;
|
|
msg.writeSmall(support);
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i)
|
|
msg.writeSmall(claims[i]);
|
|
uint cnt = offers.length;
|
|
msg.writeSmall(cnt);
|
|
for(uint i = 0; i < cnt; ++i)
|
|
msg << offers[i];
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
id = msg.readSmall();
|
|
msg >> fromEmpire;
|
|
msg >> side;
|
|
support = msg.readSmall();
|
|
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i)
|
|
claims[i] = msg.readSmall();
|
|
uint cnt = msg.readSmall();
|
|
offers.length = cnt;
|
|
for(uint i = 0; i < cnt; ++i)
|
|
msg >> offers[i];
|
|
}
|
|
};
|
|
//}}}
|
|
// {{{ Edits
|
|
export DiplomacyEdict, DiplomacyEdictType;
|
|
|
|
enum DiplomacyEdictType {
|
|
DET_None,
|
|
DET_Conquer,
|
|
};
|
|
|
|
tidy final class DiplomacyEdict : Savable, Serializable {
|
|
DiplomacyEdictType type = DET_None;
|
|
Empire@ empTarget;
|
|
Object@ objTarget;
|
|
int targetId = -1;
|
|
|
|
void clear() {
|
|
type = DET_None;
|
|
@empTarget = null;
|
|
@objTarget = null;
|
|
targetId = -1;
|
|
}
|
|
|
|
void write(Message& msg) {
|
|
msg.writeSmall(uint(type));
|
|
msg << empTarget;
|
|
msg << objTarget;
|
|
msg << targetId;
|
|
}
|
|
|
|
void read(Message& msg) {
|
|
type = DiplomacyEdictType(msg.readSmall());
|
|
msg >> empTarget;
|
|
msg >> objTarget;
|
|
msg >> targetId;
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file << uint(type);
|
|
file << empTarget;
|
|
file << objTarget;
|
|
file << targetId;
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
uint t = 0;
|
|
file >> t;
|
|
type = DiplomacyEdictType(t);
|
|
file >> empTarget;
|
|
file >> objTarget;
|
|
file >> targetId;
|
|
}
|
|
};
|
|
//}}}
|
|
// {{{ Global initialization
|
|
void readFile(const string& filename) {
|
|
ReadFile file(filename, true);
|
|
while(file++) {
|
|
bool used = false;
|
|
while(!used) {
|
|
if(file.key == "Card") {
|
|
used = readCard(file);
|
|
}
|
|
else if(file.key == "Vote") {
|
|
used = readVote(file);
|
|
}
|
|
else if(file.key == "Effect") {
|
|
used = readEffect(file);
|
|
}
|
|
else if(file.key == "Clause") {
|
|
used = readClause(file);
|
|
}
|
|
else {
|
|
file.error("Invalid line outside of block: "+escape(file.line));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void preInit() {
|
|
FileList list("data/influence", "*.txt", true);
|
|
for(uint i = 0, cnt = list.length; i < cnt; ++i)
|
|
readFile(list.path[i]);
|
|
}
|
|
|
|
void init() {
|
|
for(uint i = 0, cnt = cardList.length; i < cnt; i++)
|
|
cardList[i].init();
|
|
for(uint i = 0, cnt = voteList.length; i < cnt; i++)
|
|
voteList[i].init();
|
|
for(uint i = 0, cnt = effectList.length; i < cnt; i++)
|
|
effectList[i].init();
|
|
for(uint i = 0, cnt = clauseList.length; i < cnt; i++)
|
|
clauseList[i].init();
|
|
}
|
|
|
|
void saveIdentifiers(SaveFile& file) {
|
|
//Influence Cards
|
|
for(uint i = 0, cnt = cardList.length; i < cnt; ++i) {
|
|
auto@ type = cardList[i];
|
|
file.addIdentifier(SI_InfluenceCard, type.id, type.ident);
|
|
}
|
|
|
|
//Influence Votes
|
|
for(uint i = 0, cnt = voteList.length; i < cnt; ++i) {
|
|
auto@ type = voteList[i];
|
|
file.addIdentifier(SI_InfluenceVote, type.id, type.ident);
|
|
}
|
|
|
|
//Influence Effects
|
|
for(uint i = 0, cnt = effectList.length; i < cnt; ++i) {
|
|
auto@ type = effectList[i];
|
|
file.addIdentifier(SI_InfluenceEffect, type.id, type.ident);
|
|
}
|
|
|
|
//Influence Clauses
|
|
for(uint i = 0, cnt = clauseList.length; i < cnt; ++i) {
|
|
auto@ type = clauseList[i];
|
|
file.addIdentifier(SI_InfluenceClause, type.id, type.ident);
|
|
}
|
|
}
|
|
//}}
|