Merge branch 'damageTrample' into 'master'

DealDamage: add ExcessDamage

See merge request core-developers/forge!2784
This commit is contained in:
Michael Kamensky
2020-05-04 14:04:02 +00:00
15 changed files with 97 additions and 52 deletions

View File

@@ -989,7 +989,7 @@ public class GameAction {
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
if (c.isCreature()) {
// Rule 704.5f - Put into grave (no regeneration) for toughness <= 0
if (c.getLethal() <= 0) {
if (c.getNetToughness() <= 0) {
if (noRegCreats == null) {
noRegCreats = new CardCollection();
}
@@ -1009,7 +1009,7 @@ public class GameAction {
}
// Rule 704.5g - Destroy due to lethal damage
// Rule 704.5h - Destroy due to deathtouch
else if (c.getLethal() <= c.getDamage() || c.hasBeenDealtDeathtouchDamage()) {
else if (c.getDamage() > 0 && (c.getLethal() <= c.getDamage() || c.hasBeenDealtDeathtouchDamage())) {
if (desCreats == null) {
desCreats = new CardCollection();
}

View File

@@ -1,8 +1,10 @@
package forge.game.ability.effects;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameEntityCounterTable;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
@@ -11,6 +13,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardDamageMap;
import forge.game.card.CardUtil;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Lang;
@@ -123,6 +126,7 @@ public class DamageDealEffect extends DamageBaseEffect {
@Override
public void resolve(SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Player activationPlayer = sa.getActivatingPlayer();
final Game game = hostCard.getGame();
final String damage = sa.getParam("NumDmg");
@@ -234,6 +238,9 @@ public class DamageDealEffect extends DamageBaseEffect {
for (final Object o : tgts) {
dmg = (sa.usesTargeting() && sa.hasParam("DividedAsYouChoose")) ? sa.getTargetRestrictions().getDividedValue(o) : dmg;
if (dmg <= 0) {
continue;
}
if (o instanceof Card) {
final Card c = (Card) o;
final Card gc = game.getCardState(c, null);
@@ -247,7 +254,27 @@ public class DamageDealEffect extends DamageBaseEffect {
c.setHasBeenDealtDeathtouchDamage(false);
c.clearAssignedDamage();
} else {
c.addDamage(dmg, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa);
if (sa.hasParam("ExcessDamage") && (!sa.hasParam("ExcessDamageCondition") ||
sourceLKI.isValid(sa.getParam("ExcessDamageCondition").split(","), activationPlayer, hostCard, sa))) {
// excess damage explicit says toughness, not lethal damage in the rules
int lethal = c.getNetToughness() - c.getDamage();
if (sourceLKI.hasKeyword(Keyword.DEATHTOUCH)) {
lethal = Math.min(lethal, 1);
}
int dmgToTarget = Math.min(lethal, dmg);
c.addDamage(dmgToTarget, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa);
List<GameEntity> list = Lists.newArrayList();
list.addAll(AbilityUtils.getDefinedCards(hostCard, sa.getParam("ExcessDamage"), sa));
list.addAll(AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("ExcessDamage"), sa));
if (!list.isEmpty()) {
list.get(0).addDamage(dmg - dmgToTarget, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa);
}
} else {
c.addDamage(dmg, sourceLKI, false, noPrevention, damageMap, preventMap, counterTable, sa);
}
}
}
} else if (o instanceof Player) {

View File

@@ -4793,7 +4793,7 @@ public class Card extends GameEntity implements Comparable<Card> {
// this is the amount of damage a creature needs to receive before it dies
public final int getLethal() {
if (getAmountOfKeyword("Lethal damage dealt to CARDNAME is determined by its power rather than its toughness.") % 2 !=0) {
if (hasKeyword("Lethal damage dealt to CARDNAME is determined by its power rather than its toughness.")) {
return getNetPower(); }
else {
return getNetToughness(); }
@@ -4801,11 +4801,7 @@ public class Card extends GameEntity implements Comparable<Card> {
// this is the minimal damage a trampling creature has to assign to a blocker
public final int getLethalDamage() {
if (getAmountOfKeyword("Lethal damage dealt to CARDNAME is determined by its power rather than its toughness.") % 2 !=0) {
return getNetPower() - getDamage() - getTotalAssignedDamage();
}
else {
return getNetToughness() - getDamage() - getTotalAssignedDamage();}
return getLethal() - getDamage() - getTotalAssignedDamage();
}
public final int getDamage() {

View File

@@ -254,6 +254,7 @@ public class CardView extends GameEntityView {
}
void updateDamage(Card c) {
set(TrackableProperty.Damage, c.getDamage());
set(TrackableProperty.LethalDamage, c.getLethalDamage());
}
public int getAssignedDamage() {
@@ -261,10 +262,11 @@ public class CardView extends GameEntityView {
}
void updateAssignedDamage(Card c) {
set(TrackableProperty.AssignedDamage, c.getTotalAssignedDamage());
set(TrackableProperty.LethalDamage, c.getLethalDamage());
}
public int getLethalDamage() {
return getCurrentState().getToughness() - getDamage() - getAssignedDamage();
return get(TrackableProperty.LethalDamage);
}
public int getShieldCount() {
@@ -701,6 +703,7 @@ public class CardView extends GameEntityView {
}
void updateState(Card c) {
updateName(c);
updateDamage(c);
boolean isSplitCard = c.isSplitCard();
set(TrackableProperty.Cloned, c.isCloned());

View File

@@ -42,6 +42,7 @@ public enum TrackableProperty {
CommanderAltType(TrackableTypes.StringType),
Damage(TrackableTypes.IntegerType),
AssignedDamage(TrackableTypes.IntegerType),
LethalDamage(TrackableTypes.IntegerType),
ShieldCount(TrackableTypes.IntegerType),
ChosenType(TrackableTypes.StringType),
ChosenColors(TrackableTypes.StringListType),

View File

@@ -932,7 +932,7 @@ public final class CMatchUI
}
@Override
public Map<CardView, Integer> assignDamage(final CardView attacker,
public Map<CardView, Integer> assignCombatDamage(final CardView attacker,
final List<CardView> blockers, final int damage,
final GameEntityView defender, final boolean overrideOrder) {
if (damage <= 0) {
@@ -949,7 +949,7 @@ public final class CMatchUI
FThreads.invokeInEdtAndWait(new Runnable() {
@Override
public void run() {
final VAssignDamage v = new VAssignDamage(CMatchUI.this, attacker, blockers, damage, defender, overrideOrder);
final VAssignCombatDamage v = new VAssignCombatDamage(CMatchUI.this, attacker, blockers, damage, defender, overrideOrder);
result.set(v.getDamageMap());
}});
return result.get();

View File

@@ -6,12 +6,12 @@
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@@ -54,14 +54,14 @@ import forge.util.Localizer;
/**
* Assembles Swing components of assign damage dialog.
*
*
* This needs a JDialog to maintain a modal state.
* Without the modal state, the PhaseHandler automatically
* moves forward to phase Main2 without assigning damage.
*
* <br><br><i>(V at beginning of class name denotes a view class.)</i>
*/
public class VAssignDamage {
public class VAssignCombatDamage {
final Localizer localizer = Localizer.getInstance();
private final CMatchUI matchUI;
@@ -119,7 +119,7 @@ public class VAssignDamage {
CardView source = ((CardPanel) evt.getSource()).getCard();
if (!damage.containsKey(source)) source = null; // to get player instead of fake card
final FSkin.Colors brdrColor = VAssignDamage.this.canAssignTo(source) ? FSkin.Colors.CLR_ACTIVE : FSkin.Colors.CLR_INACTIVE;
final FSkin.Colors brdrColor = VAssignCombatDamage.this.canAssignTo(source) ? FSkin.Colors.CLR_ACTIVE : FSkin.Colors.CLR_INACTIVE;
((CardPanel) evt.getSource()).setBorder(new FSkin.LineSkinBorder(FSkin.getColor(brdrColor), 2));
}
@@ -135,13 +135,13 @@ public class VAssignDamage {
boolean meta = evt.isControlDown();
boolean isLMB = SwingUtilities.isLeftMouseButton(evt);
boolean isRMB = SwingUtilities.isRightMouseButton(evt);
if ( isLMB || isRMB)
assignDamageTo(source, meta, isLMB);
}
};
public VAssignDamage(final CMatchUI matchUI, final CardView attacker, final List<CardView> blockers, final int damage0, final GameEntityView defender0, boolean overrideOrder) {
public VAssignCombatDamage(final CMatchUI matchUI, final CardView attacker, final List<CardView> blockers, final int damage0, final GameEntityView defender0, boolean overrideOrder) {
this.matchUI = matchUI;
String s = localizer.getMessage("lbLAssignDamageDealtBy");
dlg.setTitle(s.replace("%s",attacker.toString()));
@@ -194,12 +194,12 @@ public class VAssignDamage {
CardView fakeCard = null;
if (defender instanceof CardView) {
fakeCard = (CardView)defender;
} else if (defender instanceof PlayerView) {
} else if (defender instanceof PlayerView) {
final PlayerView p = (PlayerView)defender;
fakeCard = new CardView(-1, null, defender.toString(), p, matchUI.getAvatarImage(p.getLobbyPlayerName()));
}
addPanelForDefender(pnlDefenders, fakeCard);
}
}
// Add "opponent placeholder" card if trample allowed
// If trample allowed, make card placeholder
@@ -231,7 +231,7 @@ public class VAssignDamage {
pnlMain.add(pnlButtons, "ax center, w 350px!, gap 10px 10px 10px 10px, span 2");
overlay.add(pnlMain);
pnlMain.getRootPane().setDefaultButton(btnOK);
SwingUtilities.invokeLater(new Runnable() {
@Override
@@ -271,13 +271,13 @@ public class VAssignDamage {
* @param isLMB
*/
private void assignDamageTo(CardView source, final boolean meta, final boolean isAdding) {
if ( !damage.containsKey(source) )
if ( !damage.containsKey(source) )
source = null;
// If trying to assign to the defender, follow the normal assignment rules
// No need to check for "active" creature assignee when overiding combatant order
if ((source == null || source == defender || !overrideCombatantOrder) && isAdding &&
!VAssignDamage.this.canAssignTo(source)) {
if ((source == null || source == defender || !overrideCombatantOrder) && isAdding &&
!VAssignCombatDamage.this.canAssignTo(source)) {
return;
}
@@ -285,9 +285,9 @@ public class VAssignDamage {
int lethalDamage = getDamageToKill(source);
int damageItHad = damage.get(source).damage;
int leftToKill = Math.max(0, lethalDamage - damageItHad);
int damageToAdd = isAdding ? 1 : -1;
int leftToAssign = getRemainingDamage();
// Left click adds damage, right click substracts damage.
// Hold Ctrl to assign lethal damage, Ctrl-click again on a creature with lethal damage to assign all available damage to it
@@ -298,18 +298,18 @@ public class VAssignDamage {
damageToAdd = damageItHad > lethalDamage ? lethalDamage - damageItHad : -damageItHad;
}
}
if ( damageToAdd > leftToAssign )
damageToAdd = leftToAssign;
// cannot assign first blocker less than lethal damage except when overriding order
boolean isFirstBlocker = defenders.get(0).card == source;
if (!overrideCombatantOrder && isFirstBlocker && damageToAdd + damageItHad < lethalDamage )
return;
if ( 0 == damageToAdd || damageToAdd + damageItHad < 0)
if ( 0 == damageToAdd || damageToAdd + damageItHad < 0)
return;
addDamage(source, damageToAdd);
checkDamageQueue();
updateLabels();
@@ -350,7 +350,7 @@ public class VAssignDamage {
}
if ( dmgLeft < 0 )
throw new RuntimeException("initialAssignDamage managed to assign more damage than it could");
if (toAllBlockers && dmgLeft > 0) {
if (toAllBlockers && dmgLeft > 0) {
// flush the remaining damage into last defender if assigning all damage
addDamage(dtLast.card, dmgLeft );
}
@@ -362,7 +362,7 @@ public class VAssignDamage {
for(DamageTarget dt : defenders)
dt.damage = 0;
}
private void addDamage(final CardView card, int addedDamage) {
// If we don't have enough left or we're trying to unassign too much return
final int canAssign = getRemainingDamage();
@@ -371,7 +371,7 @@ public class VAssignDamage {
}
final DamageTarget dt = damage.get(card);
dt.damage = Math.max(0, addedDamage + dt.damage);
dt.damage = Math.max(0, addedDamage + dt.damage);
}
@@ -393,7 +393,7 @@ public class VAssignDamage {
int damageLeft = totalDamageToAssign;
boolean allHaveLethal = true;
for ( DamageTarget dt : defenders )
{
int dmg = dt.damage;
@@ -402,9 +402,9 @@ public class VAssignDamage {
int overkill = dmg - lethal;
StringBuilder sb = new StringBuilder();
sb.append(dmg);
if( overkill >= 0 ) {
if( overkill >= 0 ) {
sb.append(" (").append(localizer.getMessage("lblLethal"));
if( overkill > 0 )
if( overkill > 0 )
sb.append(" +").append(overkill);
sb.append(")");
}
@@ -421,9 +421,9 @@ public class VAssignDamage {
// assigned dynamically, the cards die off and further damage to them can't
// be modified.
private void finish() {
if ( getRemainingDamage() > 0 )
if ( getRemainingDamage() > 0 )
return;
dlg.dispose();
SOverlayUtils.hideOverlay();
}
@@ -441,7 +441,10 @@ public class VAssignDamage {
}
}
else {
lethalDamage = VAssignDamage.this.attackerHasDeathtouch ? 1 : Math.max(0, card.getLethalDamage());
lethalDamage = Math.max(0, card.getLethalDamage());
if (attackerHasDeathtouch) {
lethalDamage = Math.min(lethalDamage, 1);
}
}
return lethalDamage;
}

View File

@@ -45,7 +45,7 @@ import forge.player.PlayerZoneUpdate;
import forge.player.PlayerZoneUpdates;
import forge.properties.ForgePreferences;
import forge.properties.ForgePreferences.FPref;
import forge.screens.match.views.VAssignDamage;
import forge.screens.match.views.VAssignCombatDamage;
import forge.screens.match.views.VPhaseIndicator;
import forge.screens.match.views.VPhaseIndicator.PhaseLabel;
import forge.screens.match.views.VPlayerPanel;
@@ -172,7 +172,7 @@ public class MatchController extends AbstractGuiGame {
public void showPromptMessage(final PlayerView player, final String message) {
view.getPrompt(player).setMessage(message);
}
@Override
public void showPromptMessage(final PlayerView player, final String message, final CardView card) {
view.getPrompt(player).setMessage(message, card);
@@ -360,11 +360,11 @@ public class MatchController extends AbstractGuiGame {
}
@Override
public Map<CardView, Integer> assignDamage(final CardView attacker, final List<CardView> blockers, final int damage, final GameEntityView defender, final boolean overrideOrder) {
public Map<CardView, Integer> assignCombatDamage(final CardView attacker, final List<CardView> blockers, final int damage, final GameEntityView defender, final boolean overrideOrder) {
return new WaitCallback<Map<CardView, Integer>>() {
@Override
public void run() {
final VAssignDamage v = new VAssignDamage(attacker, blockers, damage, defender, overrideOrder, this);
final VAssignCombatDamage v = new VAssignCombatDamage(attacker, blockers, damage, defender, overrideOrder, this);
v.show();
}
}.invokeAndWait();

View File

@@ -53,7 +53,7 @@ import java.util.Map;
import com.badlogic.gdx.utils.Align;
public class VAssignDamage extends FDialog {
public class VAssignCombatDamage extends FDialog {
private static final float CARD_GAP_X = Utils.scale(10);
private static final float ADD_BTN_HEIGHT = Utils.AVG_FINGER_HEIGHT * 0.75f;
@@ -95,7 +95,7 @@ public class VAssignDamage extends FDialog {
* @param defender GameEntity that's bein attacked
* @param overrideOrder override combatant order
*/
public VAssignDamage(final CardView attacker, final List<CardView> blockers, final int damage0, final GameEntityView defender0, boolean overrideOrder, final WaitCallback<Map<CardView, Integer>> waitCallback) {
public VAssignCombatDamage(final CardView attacker, final List<CardView> blockers, final int damage0, final GameEntityView defender0, boolean overrideOrder, final WaitCallback<Map<CardView, Integer>> waitCallback) {
super(Localizer.getInstance().getMessage("lbLAssignDamageDealtBy").replace("%s",CardTranslation.getTranslatedName(attacker.getName())) , 3);
callback = waitCallback;
@@ -453,7 +453,10 @@ public class VAssignDamage extends FDialog {
}
}
else {
lethalDamage = attackerHasDeathtouch ? 1 : Math.max(0, source.getLethalDamage());
lethalDamage = Math.max(0, source.getLethalDamage());
if (attackerHasDeathtouch) {
lethalDamage = Math.min(lethalDamage, 1);
}
}
return lethalDamage;
}

View File

@@ -0,0 +1,5 @@
Name:Flame Spill
ManaCost:2 R
Types:Instant
A:SP$ DealDamage | Cost$ 2 R | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumDmg$ 4 | ExcessDamage$ TargetedController | SpellDescription$ CARDNAME deals 4 damage to target creature. Excess damage is dealt to that creatures controller instead.
Oracle:Flame Spill deals 4 damage to target creature. Excess damage is dealt to that creatures controller instead.

View File

@@ -0,0 +1,7 @@
Name:Ram Through
ManaCost:1 G
Types:Instant
A:SP$ Pump | Cost$ 1 G | ValidTgts$ Creature.YouCtrl | AILogic$ PowerDmg | TgtPrompt$ Select target creature you control | SubAbility$ SoulsDamage | StackDescription$ SpellDescription | SpellDescription$ Target creature you control deals damage equal to its power to target creature you dont control. If the creature you control has trample, excess damage is dealt to that creatures controller instead.
SVar:SoulsDamage:DB$ DealDamage | ValidTgts$ Creature.YouDontCtrl | AILogic$ PowerDmg | TgtPrompt$ Select target creature you don't control | NumDmg$ X | References$ X | ConditionDefined$ Targeted | ConditionPresent$ Creature | ConditionCompare$ EQ1 | DamageSource$ ParentTarget | ExcessDamage$ TargetedController | ExcessDamageCondition$ Card.withTrample | StackDescription$ None
SVar:X:ParentTargeted$CardPower
Oracle:Target creature you control deals damage equal to its power to target creature you dont control. If the creature you control has trample, excess damage is dealt to that creatures controller instead.

View File

@@ -62,7 +62,7 @@ public interface IGuiGame {
void updateLives(Iterable<PlayerView> livesUpdate);
void setPanelSelection(CardView hostCard);
SpellAbilityView getAbilityToPlay(CardView hostCard, List<SpellAbilityView> abilities, ITriggerEvent triggerEvent);
Map<CardView, Integer> assignDamage(CardView attacker, List<CardView> blockers, int damage, GameEntityView defender, boolean overrideOrder);
Map<CardView, Integer> assignCombatDamage(CardView attacker, List<CardView> blockers, int damage, GameEntityView defender, boolean overrideOrder);
void message(String message);
void message(String message, String title);

View File

@@ -54,7 +54,7 @@ public enum ProtocolMethod {
updateLives (Mode.SERVER, Void.TYPE, Iterable/*PlayerView*/.class),
setPanelSelection (Mode.SERVER, Void.TYPE, CardView.class),
getAbilityToPlay (Mode.SERVER, SpellAbilityView.class, CardView.class, List/*SpellAbilityView*/.class, ITriggerEvent.class),
assignDamage (Mode.SERVER, Map.class, CardView.class, List/*CardView*/.class, Integer.TYPE, GameEntityView.class, Boolean.TYPE),
assignCombatDamage (Mode.SERVER, Map.class, CardView.class, List/*CardView*/.class, Integer.TYPE, GameEntityView.class, Boolean.TYPE),
message (Mode.SERVER, Void.TYPE, String.class, String.class),
showErrorDialog (Mode.SERVER, Void.TYPE, String.class, String.class),
showConfirmDialog (Mode.SERVER, Boolean.TYPE, String.class, String.class, String.class, String.class, Boolean.TYPE),

View File

@@ -195,8 +195,8 @@ public class NetGuiGame extends AbstractGuiGame {
}
@Override
public Map<CardView, Integer> assignDamage(final CardView attacker, final List<CardView> blockers, final int damage, final GameEntityView defender, final boolean overrideOrder) {
return sendAndWait(ProtocolMethod.assignDamage, attacker, blockers, damage, defender, overrideOrder);
public Map<CardView, Integer> assignCombatDamage(final CardView attacker, final List<CardView> blockers, final int damage, final GameEntityView defender, final boolean overrideOrder) {
return sendAndWait(ProtocolMethod.assignCombatDamage, attacker, blockers, damage, defender, overrideOrder);
}
@Override

View File

@@ -291,7 +291,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
if ((attacker.hasKeyword(Keyword.TRAMPLE) && defender != null) || (blockers.size() > 1)) {
final CardView vAttacker = CardView.get(attacker);
final GameEntityView vDefender = GameEntityView.get(defender);
final Map<CardView, Integer> result = getGui().assignDamage(vAttacker, vBlockers, damageDealt,
final Map<CardView, Integer> result = getGui().assignCombatDamage(vAttacker, vBlockers, damageDealt,
vDefender, overrideOrder);
for (final Entry<CardView, Integer> e : result.entrySet()) {
map.put(game.getCard(e.getKey()), e.getValue());