Face down images and cards rework (#11873)

Face down changes:
* GUI: added visible face down type and real card name for controller/owner (opponent can see it after game ends);
* GUI: added day/night button to view real card for controller/owner (opponent can see it after game ends);
* game: fixed that faced-down card can render symbols, abilities and other hidden data from a real card;
* images: added image support for normal faced-down cards;
* images: added image support for morph and megamorph faced-down cards;
* images: added image support for foretell faced-down cards;

Other changes:
* images: fixed missing tokens from DDD set;
* images: no more client restart to apply newly downloaded images or render settings;
* images: improved backface image quality (use main menu -> symbols to download it);
This commit is contained in:
Oleg Agafonov 2024-02-29 01:14:54 +04:00 committed by GitHub
parent 4901de12c1
commit e38a79f231
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 2178 additions and 1495 deletions

View file

@ -1,7 +1,6 @@
package mage.game;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.keyword.TransformAbility;
import mage.cards.*;
import mage.constants.Outcome;
@ -27,7 +26,7 @@ public final class ZonesHandler {
public static boolean cast(ZoneChangeInfo info, Ability source, Game game) {
if (maybeRemoveFromSourceZone(info, game, source)) {
placeInDestinationZone(info,0, source, game);
placeInDestinationZone(info, 0, source, game);
// create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx))
Set<Card> cards = new HashSet<>();
Set<PermanentToken> tokens = new HashSet<>();
@ -38,12 +37,12 @@ public final class ZonesHandler {
cards.add(targetCard);
}
game.fireEvent(new ZoneChangeGroupEvent(
cards,
tokens,
info.event.getSourceId(),
info.event.getSource(),
info.event.getPlayerId(),
info.event.getFromZone(),
cards,
tokens,
info.event.getSourceId(),
info.event.getSource(),
info.event.getPlayerId(),
info.event.getFromZone(),
info.event.getToZone()));
// normal movement
game.fireEvent(info.event);
@ -325,33 +324,50 @@ public final class ZonesHandler {
// Handle all normal cases
Card card = getTargetCard(game, event.getTargetId());
if (card == null) {
// If we can't find the card we can't remove it.
// if we can't find the card we can't remove it.
return false;
}
boolean success = false;
boolean isGoodToMove = false;
if (info.faceDown) {
card.setFaceDown(true, game);
// any card can be moved as face down (doubled faced cards also support face down)
isGoodToMove = true;
} else if (event.getToZone().equals(Zone.BATTLEFIELD)) {
if (!card.isPermanent(game)
&& (!card.isTransformable() || Boolean.FALSE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId())))) {
// Non permanents (Instants, Sorceries, ... stay in the zone they are if an abilty/effect tries to move it to the battlefield
return false;
}
// non-permanents can't move to battlefield
// "return to battlefield transformed" abilities uses game state value instead "info.transformed", so check it too
// TODO: possible bug with non permanent on second side like Life // Death, see https://github.com/magefree/mage/issues/11573
// need to check second side here, not status only
// TODO: possible bug with Nightbound, search all usage of getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED and insert additional check Ability.checkCard
boolean wantToPutTransformed = card.isTransformable()
&& Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()));
isGoodToMove = card.isPermanent(game) || wantToPutTransformed;
} else {
// other zones allows to move
isGoodToMove = true;
}
if (!isGoodToMove) {
return false;
}
// TODO: is it buggy? Card characteristics are global - if you change face down then it will be
// changed in original card too, not in blueprint only
card.setFaceDown(info.faceDown, game);
boolean success = false;
if (!game.replaceEvent(event)) {
Zone fromZone = event.getFromZone();
if (event.getToZone() == Zone.BATTLEFIELD) {
// prepare card and permanent
// If needed take attributes from the spell (e.g. color of spell was changed)
card = takeAttributesFromSpell(card, event, game);
// PUT TO BATTLEFIELD AS PERMANENT
// prepare card and permanent (card must contain full data, even for face down)
// if needed to take attributes from the spell (e.g. color of spell was changed)
card = prepareBlueprintCardFromSpell(card, event, game);
// controlling player can be replaced so use event player now
Permanent permanent;
if (card instanceof MeldCard) {
permanent = new PermanentMeld(card, event.getPlayerId(), game);
} else if (card instanceof ModalDoubleFacedCard) {
// main mdf card must be processed before that call (e.g. only halfes can be moved to battlefield)
// main mdf card must be processed before that call (e.g. only halves can be moved to battlefield)
throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half");
} else if (card instanceof Permanent) {
throw new IllegalStateException("Unexpected trying of move permanent to battlefield instead card");
@ -361,11 +377,12 @@ public final class ZonesHandler {
// put onto battlefield with possible counters
game.getPermanentsEntering().put(permanent.getId(), permanent);
card.checkForCountersToAdd(permanent, source, game);
card.applyEnterWithCounters(permanent, source, game);
permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield
&& ((ZoneChangeInfo.Battlefield) info).tapped);
// if need prototyped version
if (Zone.STACK == event.getFromZone()) {
Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null) {
@ -375,29 +392,34 @@ public final class ZonesHandler {
permanent.setFaceDown(info.faceDown, game);
if (info.faceDown) {
card.setFaceDown(false, game);
// TODO: need research cards with "setFaceDown(false"
// TODO: delete after new release and new face down bugs (old code remove face down status from a card for unknown reason), 2024-02-20
//card.setFaceDown(false, game);
}
// make sure the controller of all continuous effects of this card are switched to the current controller
game.setScopeRelevant(true);
game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId());
if (permanent.entersBattlefield(source, game, fromZone, true)
&& card.removeFromZone(game, fromZone, source)) {
success = true;
event.setTarget(permanent);
} else {
// revert controller to owner if permanent does not enter
game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId());
game.getPermanentsEntering().remove(permanent.getId());
try {
game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId());
if (permanent.entersBattlefield(source, game, fromZone, true)
&& card.removeFromZone(game, fromZone, source)) {
success = true;
event.setTarget(permanent);
} else {
// revert controller to owner if permanent does not enter
game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId());
game.getPermanentsEntering().remove(permanent.getId());
}
} finally {
game.setScopeRelevant(false);
}
game.setScopeRelevant(false);
} else if (event.getTarget() != null) {
card.setFaceDown(info.faceDown, game);
// PUT PERMANENT TO OTHER ZONE (e.g. remove only)
Permanent target = event.getTarget();
success = target.removeFromZone(game, fromZone, source)
&& game.getPlayer(target.getControllerId()).removeFromBattlefield(target, source, game);
} else {
card.setFaceDown(info.faceDown, game);
// PUT CARD TO OTHER ZONE
success = card.removeFromZone(game, fromZone, source);
}
}
@ -434,17 +456,30 @@ public final class ZonesHandler {
return order;
}
private static Card takeAttributesFromSpell(Card card, ZoneChangeEvent event, Game game) {
private static Card prepareBlueprintCardFromSpell(Card card, ZoneChangeEvent event, Game game) {
card = card.copy();
if (Zone.STACK == event.getFromZone()) {
// TODO: wtf, why only colors!? Must research and remove colors workaround or add all other data like types too
Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null && !spell.isFaceDown(game)) {
// TODO: wtf, why only colors!? Must research and remove colors workaround
// old version
if (false && spell != null && !spell.isFaceDown(game)) {
if (!card.getColor(game).equals(spell.getColor(game))) {
// the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card
card.getColor(game).setColor(spell.getColor(game));
}
}
// new version
if (true && spell != null && spell.getSpellAbility() != null) {
Card characteristics = spell.getSpellAbility().getCharacteristics(game);
if (!characteristics.isFaceDown(game)) {
if (!card.getColor(game).equals(characteristics.getColor(game))) {
// TODO: don't work with prototyped spells (setColor can't set colorless color)
card.getColor(game).setColor(characteristics.getColor(game));
}
}
}
}
return card;
}