package games.strategy.triplea.attachments;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import games.strategy.engine.data.Attachable;
import games.strategy.engine.data.CompositeChange;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.GameParseException;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.annotations.GameProperty;
import games.strategy.engine.data.annotations.InternalDoNotExport;
import games.strategy.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.engine.random.IRandomStats.DiceType;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.player.ITripleAPlayer;
import games.strategy.util.Match;
import games.strategy.util.ThreadUtil;
import games.strategy.util.Tuple;
public abstract class AbstractTriggerAttachment extends AbstractConditionsAttachment {
private static final long serialVersionUID = 5866039180681962697L;
public static final String NOTIFICATION = "Notification";
public static final String AFTER = "after";
public static final String BEFORE = "before";
// "setTrigger" is also a valid setter, and it just calls "setConditions" in AbstractConditionsAttachment. Kept for
// backwards
// compatibility.
private int m_uses = -1;
@InternalDoNotExport
// Do Not Export (do not include in IAttachment).
private boolean m_usedThisRound = false;
private String m_notification = null;
private ArrayList<Tuple<String, String>> m_when = new ArrayList<>();
public AbstractTriggerAttachment(final String name, final Attachable attachable, final GameData gameData) {
super(name, attachable, gameData);
}
public static CompositeChange triggerSetUsedForThisRound(final PlayerID player, final IDelegateBridge aBridge) {
final CompositeChange change = new CompositeChange();
for (final TriggerAttachment ta : TriggerAttachment.getTriggers(player, aBridge.getData(), null)) {
if (ta.getUsedThisRound()) {
final int currentUses = ta.getUses();
if (currentUses > 0) {
change.add(ChangeFactory.attachmentPropertyChange(ta, Integer.toString(currentUses - 1), "uses"));
change.add(ChangeFactory.attachmentPropertyChange(ta, false, "usedThisRound"));
}
}
}
return change;
}
/**
* Adds to, not sets. Anything that adds to instead of setting needs a clear function as well.
* DO NOT REMOVE THIS (or else you will break a lot of older xmls)
*
* @deprecated please use setConditions, getConditions, clearConditions, instead.
*/
@Deprecated
@GameProperty(xmlProperty = true, gameProperty = false, adds = true)
public void setTrigger(final String conditions) throws GameParseException {
setConditions(conditions);
}
/**
* @deprecated please use setConditions, getConditions, clearConditions, instead.
*/
@Deprecated
public List<RulesAttachment> getTrigger() {
return getConditions();
}
/**
* @deprecated please use setConditions, getConditions, clearConditions, instead.
*/
@Deprecated
public void clearTrigger() {
clearConditions();
}
/**
* @deprecated please use setConditions, getConditions, clearConditions, instead.
*/
@Deprecated
public void resetTrigger() {
resetConditions();
}
@GameProperty(xmlProperty = true, gameProperty = true, adds = false)
public void setUses(final String s) {
m_uses = getInt(s);
}
@GameProperty(xmlProperty = true, gameProperty = true, adds = false)
public void setUses(final Integer u) {
m_uses = u;
}
@GameProperty(xmlProperty = true, gameProperty = true, adds = false)
public void setUses(final int u) {
m_uses = u;
}
@GameProperty(xmlProperty = false, gameProperty = true, adds = false)
public void setUsedThisRound(final String s) {
m_usedThisRound = getBool(s);
}
@GameProperty(xmlProperty = false, gameProperty = true, adds = false)
public void setUsedThisRound(final boolean usedThisRound) {
m_usedThisRound = usedThisRound;
}
@GameProperty(xmlProperty = false, gameProperty = true, adds = false)
public void setUsedThisRound(final Boolean usedThisRound) {
m_usedThisRound = usedThisRound;
}
public boolean getUsedThisRound() {
return m_usedThisRound;
}
public void resetUsedThisRound() {
m_usedThisRound = false;
}
public int getUses() {
return m_uses;
}
public void resetUses() {
m_uses = -1;
}
/**
* Adds to, not sets. Anything that adds to instead of setting needs a clear function as well.
*/
@GameProperty(xmlProperty = true, gameProperty = true, adds = true)
public void setWhen(final String when) throws GameParseException {
final String[] s = when.split(":");
if (s.length != 2) {
throw new GameParseException("when must exist in 2 parts: \"before/after:stepName\"." + thisErrorMsg());
}
if (!(s[0].equals(AFTER) || s[0].equals(BEFORE))) {
throw new GameParseException("when must start with: " + BEFORE + " or " + AFTER + thisErrorMsg());
}
m_when.add(Tuple.of(s[0], s[1]));
}
@GameProperty(xmlProperty = true, gameProperty = true, adds = false)
public void setWhen(final ArrayList<Tuple<String, String>> value) {
m_when = value;
}
public ArrayList<Tuple<String, String>> getWhen() {
return m_when;
}
public void clearWhen() {
m_when.clear();
}
public void resetWhen() {
m_when = new ArrayList<>();
}
@GameProperty(xmlProperty = true, gameProperty = true, adds = false)
public void setNotification(final String sNotification) {
if (sNotification == null) {
m_notification = null;
return;
}
m_notification = sNotification;
}
public String getNotification() {
return m_notification;
}
public void resetNotification() {
m_notification = null;
}
protected void use(final IDelegateBridge aBridge) {
// instead of using up a "use" with every action, we will instead use up a "use" if the trigger is fired during this
// round
// this is in order to let a trigger that contains multiple actions, fire all of them in a single use
// we only do this for things that do not have m_when set. triggers with m_when set have their uses modified
// elsewhere.
if (!m_usedThisRound && m_uses > 0 && m_when.isEmpty()) {
aBridge.addChange(ChangeFactory.attachmentPropertyChange(this, true, "usedThisRound"));
}
}
protected boolean testChance(final IDelegateBridge aBridge) {
// "chance" should ALWAYS be checked last! (always check all other conditions first)
final int hitTarget = getChanceToHit();
final int diceSides = getChanceDiceSides();
if (diceSides <= 0 || hitTarget >= diceSides) {
changeChanceDecrementOrIncrementOnSuccessOrFailure(aBridge, true, false);
return true;
} else if (hitTarget <= 0) {
changeChanceDecrementOrIncrementOnSuccessOrFailure(aBridge, false, false);
return false;
}
// there is an issue with maps using thousands of chance triggers: they are causing the cypted random source (ie:
// live and pbem games) to lock up or error out
// so we need to slow them down a bit, until we come up with a better solution (like aggregating all the chances
// together, then getting a ton of random numbers at once instead of one at a time)
ThreadUtil.sleep(100);
final int rollResult = aBridge.getRandom(diceSides, null, DiceType.ENGINE,
"Attempting the Trigger: " + MyFormatter.attachmentNameToText(this.getName())) + 1;
final boolean testChance = rollResult <= hitTarget;
final String notificationMessage =
(testChance ? TRIGGER_CHANCE_SUCCESSFUL : TRIGGER_CHANCE_FAILURE) + " (Rolled at " + hitTarget + " out of "
+ diceSides + " Result: " + rollResult + " for " + MyFormatter.attachmentNameToText(this.getName()) + ")";
aBridge.getHistoryWriter().startEvent(notificationMessage);
changeChanceDecrementOrIncrementOnSuccessOrFailure(aBridge, testChance, true);
((ITripleAPlayer) aBridge.getRemotePlayer(aBridge.getPlayerID())).reportMessage(notificationMessage,
notificationMessage);
return testChance;
}
public static Match<TriggerAttachment> isSatisfiedMatch(final HashMap<ICondition, Boolean> testedConditions) {
return new Match<TriggerAttachment>() {
@Override
public boolean match(final TriggerAttachment t) {
return t.isSatisfied(testedConditions);
}
};
}
/**
* If t.getWhen() is empty, and beforeOrAfter and stepName are both null, then this returns true.
* Otherwise, all must be not null, and one of when's values must match the arguments.
*
* @param beforeOrAfter
* can be null, or must be "before" or "after"
* @param stepName
* can be null, or must be exact name of a specific stepName
* @return true if when and both args are null, and true if all are not null and when matches the args, otherwise
* false
*/
public static Match<TriggerAttachment> whenOrDefaultMatch(final String beforeOrAfter, final String stepName) {
return new Match<TriggerAttachment>() {
@Override
public boolean match(final TriggerAttachment t) {
if (beforeOrAfter == null && stepName == null && t.getWhen().isEmpty()) {
return true;
} else if (beforeOrAfter != null && stepName != null && !t.getWhen().isEmpty()) {
for (final Tuple<String, String> w : t.getWhen()) {
if (beforeOrAfter.equals(w.getFirst()) && stepName.equals(w.getSecond())) {
return true;
}
}
}
return false;
}
};
}
public static Match<TriggerAttachment> availableUses = new Match<TriggerAttachment>() {
@Override
public boolean match(final TriggerAttachment t) {
return t.getUses() != 0;
}
};
public static Match<TriggerAttachment> notificationMatch() {
return new Match<TriggerAttachment>() {
@Override
public boolean match(final TriggerAttachment t) {
return t.getNotification() != null;
}
};
}
protected static String getValueFromStringArrayForAllSubStrings(final String[] s) {
final StringBuilder sb = new StringBuilder();
for (final String subString : s) {
sb.append(":");
sb.append(subString);
}
// remove leading colon
if (sb.length() > 0 && sb.substring(0, 1).equals(":")) {
sb.replace(0, 1, "");
}
return sb.toString();
}
protected static String getValueFromStringArrayForAllExceptLastSubstring(final String[] s) {
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length - 1; i++) {
sb.append(":");
sb.append(s[i]);
}
// remove leading colon
if (sb.length() > 0 && sb.substring(0, 1).equals(":")) {
sb.replace(0, 1, "");
}
return sb.toString();
}
public static int getEachMultiple(final AbstractTriggerAttachment t) {
int eachMultiple = 1;
for (final RulesAttachment condition : t.getConditions()) {
final int tempEach = condition.getEachMultiple();
if (tempEach > eachMultiple) {
eachMultiple = tempEach;
}
}
return eachMultiple;
}
@Override
public void validate(final GameData data) throws GameParseException {
super.validate(data);
if (m_conditions == null) {
throw new GameParseException("must contain at least one condition: " + thisErrorMsg());
}
}
}