* Kicker abilities - fixed that some cards don't see kicked status of multikicker spells (example: Hallar, the Firefletcher, #4895);

This commit is contained in:
Oleg Agafonov 2020-12-15 21:00:39 +04:00
parent 4d362d7edc
commit 0c2c33e940
6 changed files with 99 additions and 17 deletions

View file

@ -278,6 +278,39 @@ public class KickerTest extends CardTestPlayerBase {
assertGraveyardCount(playerA, "Sunscape Battlemage", 1); assertGraveyardCount(playerA, "Sunscape Battlemage", 1);
} }
@Test
public void test_Conditional_TriggeredAbilityMustSeeMultikickedStatus() {
// bug:
// Hallar Not Procing Right: When I kick Thornscape Battlemage it doesn't proc Hallar effect for some reason.
// I tried this 3 times and it never triggered properly.
// Kicker {R} and/or {W} (You may pay an additional {R} and/or {W} as you cast this spell.)
// When Thornscape Battlemage enters the battlefield, if it was kicked with its {R} kicker, it deals 2 damage to any target.
// When Thornscape Battlemage enters the battlefield, if it was kicked with its {W} kicker, destroy target artifact.
addCard(Zone.HAND, playerA, "Thornscape Battlemage", 1); // {2}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
//
// Whenever you cast a spell, if that spell was kicked, put a +1/+1 counter on Hallar, the Firefletcher,
// then Hallar deals damage equal to the number of +1/+1 counters on it to each opponent.
addCard(Zone.BATTLEFIELD, playerA, "Hallar, the Firefletcher", 1);
// cast kicked spell
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thornscape Battlemage");
setChoice(playerA, "Yes"); // use kicker {R} - 2 damage to any target
setChoice(playerA, "No"); // not use kicker {W} - destroy target
addTarget(playerA, playerB); // target for 2 damage
setChoice(playerA, "Yes"); // put counter on hallar
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertCounterCount(playerA, "Hallar, the Firefletcher", CounterType.P1P1, 1);
assertLife(playerB, 20 - 2 - 1); // 2 damage from kicked spell, 1 damage from hallar's trigger
}
@Test @Test
public void test_ZCC_ReturnedPermanentMustNotBeKicked() { public void test_ZCC_ReturnedPermanentMustNotBeKicked() {
// bug: // bug:

View file

@ -1869,6 +1869,20 @@ public class TestPlayer implements Player {
printAbilities(game, computerPlayer.getPlayable(game, true)); printAbilities(game, computerPlayer.getPlayable(game, true));
printEnd(); printEnd();
} }
if (choiceType.equals("choice")) {
printStart("Unused choices");
if (!choices.isEmpty()) {
System.out.println(String.join("\n", choices));
}
printEnd();
}
if (choiceType.equals("target")) {
printStart("Unused targets");
if (targets.isEmpty()) {
System.out.println(String.join("\n", targets));
}
printEnd();
}
Assert.fail("Missing " + choiceType.toUpperCase(Locale.ENGLISH) + " def for" Assert.fail("Missing " + choiceType.toUpperCase(Locale.ENGLISH) + " def for"
+ " turn " + game.getTurnNum() + " turn " + game.getTurnNum()
+ ", step " + (game.getStep() != null ? game.getStep().getType().name() : "not started") + ", step " + (game.getStep() != null ? game.getStep().getType().name() : "not started")

View file

@ -27,7 +27,7 @@ public enum KickedCondition implements Condition {
if (card != null) { if (card != null) {
for (Ability ability : card.getAbilities()) { for (Ability ability : card.getAbilities()) {
if (ability instanceof KickerAbility) { if (ability instanceof KickerAbility) {
if (((KickerAbility) ability).isKicked(game, source, "")) { if (((KickerAbility) ability).isKicked(game, source)) {
return true; return true;
} }
} }

View file

@ -36,7 +36,6 @@ public enum GetKickerXValue implements DynamicValue {
.stream() .stream()
.anyMatch(varCost -> !((OptionalAdditionalCostImpl) varCost).getVariableCosts().isEmpty()); .anyMatch(varCost -> !((OptionalAdditionalCostImpl) varCost).getVariableCosts().isEmpty());
if (haveVarCost) { if (haveVarCost) {
int kickedCount = ((KickerAbility) ability).getKickedCounter(game, sourceAbility); int kickedCount = ((KickerAbility) ability).getKickedCounter(game, sourceAbility);
if (kickedCount > 0) { if (kickedCount > 0) {

View file

@ -122,27 +122,63 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
} }
} }
public int getKickedCounter(Game game, Ability source) { private int getKickedCounterStrict(Game game, Ability source, String needKickerCost) {
String key = getActivationKey(source, "", game); String key;
return activations.getOrDefault(key, 0); if (needKickerCost.isEmpty()) {
// need all kickers
key = getActivationKey(source, "", game);
} else {
// need only cost related kickers
key = getActivationKey(source, needKickerCost, game);
} }
public boolean isKicked(Game game, Ability source, String costText) { int totalActivations = 0;
String key = getActivationKey(source, costText, game);
if (kickerCosts.size() > 1) { if (kickerCosts.size() > 1) {
for (String activationKey : activations.keySet()) { for (String activationKey : activations.keySet()) {
if (activationKey.startsWith(key) if (activationKey.startsWith(key) && activations.get(activationKey) > 0) {
&& activations.get(activationKey) > 0) { totalActivations += activations.get(activationKey);
return true;
} }
} }
} else { } else {
if (activations.containsKey(key)) { if (activations.containsKey(key) && activations.get(key) > 0) {
return activations.get(key) > 0; totalActivations += activations.get(key);
}
}
return totalActivations;
}
/**
* Return total kicker activations (kicker + multikicker)
*
* @param game
* @param source
* @return
*/
public int getKickedCounter(Game game, Ability source) {
return getKickedCounterStrict(game, source, "");
} }
/**
* If spell was kicked
*
* @param game
* @param source
* @return
*/
public boolean isKicked(Game game, Ability source) {
return isKicked(game, source, "");
} }
return false;
/**
* If spell was kicked by specific kicker cost
*
* @param game
* @param source
* @param needKickerCost use cost.getText(true)
* @return
*/
public boolean isKicked(Game game, Ability source, String needKickerCost) {
return getKickedCounterStrict(game, source, needKickerCost) > 0;
} }
public List<OptionalAdditionalCost> getKickerCosts() { public List<OptionalAdditionalCost> getKickerCosts() {
@ -196,7 +232,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo
while (player.canRespond() && again) { while (player.canRespond() && again) {
String times = ""; String times = "";
if (kickerCost.isRepeatable()) { if (kickerCost.isRepeatable()) {
int activatedCount = getKickedCounter(game, ability); int activatedCount = getKickedCounterStrict(game, ability, kickerCost.getText(true));
times = (activatedCount + 1) + (activatedCount == 0 ? " time " : " times "); times = (activatedCount + 1) + (activatedCount == 0 ? " time " : " times ");
} }
// TODO: add AI support to find max number of possible activations (from available mana) // TODO: add AI support to find max number of possible activations (from available mana)

View file

@ -26,7 +26,7 @@ public class KickerWithAnyNumberModesAbility extends KickerAbility implements Op
@Override @Override
public void changeModes(Ability ability, Game game) { public void changeModes(Ability ability, Game game) {
if (!isKicked(game, ability, "")) { if (!isKicked(game, ability)) {
return; return;
} }