mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
game engine, tests and other fixes:
tests: fixed wrong permanent structure for battlefield cards (addCard command); tests: added docs and additional runtime checks; game: Modal double-faced cards - improved support, no more other side effects on battlefield; game: Copy abilities - improved stability and cards support; game: Player under control - improved stability and related cards support (possible NPE errors, additional runtime checks); server: fixed bloated logs with game timer; AI: fixed wrong timer in computer games;
This commit is contained in:
parent
824e4c6b7a
commit
229e8d3075
35 changed files with 303 additions and 151 deletions
|
|
@ -798,7 +798,7 @@ public final class SystemUtil {
|
|||
// TODO: replace by player.move?
|
||||
switch (zone) {
|
||||
case BATTLEFIELD:
|
||||
CardUtil.putCardOntoBattlefieldWithEffects(source, game, card, player);
|
||||
CardUtil.putCardOntoBattlefieldWithEffects(source, game, card, player, false);
|
||||
break;
|
||||
case LIBRARY:
|
||||
card.setZone(Zone.LIBRARY, game);
|
||||
|
|
|
|||
|
|
@ -1301,7 +1301,6 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
@Override
|
||||
public boolean priority(Game game) {
|
||||
game.resumeTimer(getTurnControlledBy());
|
||||
log.debug("priority");
|
||||
boolean result = priorityPlay(game);
|
||||
game.pauseTimer(getTurnControlledBy());
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import mage.players.Player;
|
|||
import mage.server.Main;
|
||||
import mage.server.User;
|
||||
import mage.server.managers.ManagerFactory;
|
||||
import mage.server.util.Splitter;
|
||||
import mage.util.MultiAmountMessage;
|
||||
import mage.utils.StreamUtils;
|
||||
import mage.utils.timer.PriorityTimer;
|
||||
|
|
@ -174,8 +173,8 @@ public class GameController implements GameCallback {
|
|||
timer.pause();
|
||||
break;
|
||||
}
|
||||
} catch (MageException ex) {
|
||||
logger.fatal("Table event listener error ", ex);
|
||||
} catch (MageException e) {
|
||||
logger.fatal("Table event listener error: " + e, e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -992,30 +991,25 @@ public class GameController implements GameCallback {
|
|||
}
|
||||
|
||||
private void perform(UUID playerId, Command command, boolean informOthers) {
|
||||
if (game.getPlayer(playerId).isGameUnderControl()) { // is the player controlling it's own turn
|
||||
if (gameSessions.containsKey(playerId)) {
|
||||
setupTimeout(playerId);
|
||||
command.execute(playerId);
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player == null) {
|
||||
throw new IllegalArgumentException("Can't perform command for unknown player id: " + playerId);
|
||||
}
|
||||
|
||||
Player realPlayerController = game.getPlayer(player.getTurnControlledBy());
|
||||
if (realPlayerController == null) {
|
||||
throw new IllegalArgumentException("Can't find real turn controller for player id: " + playerId);
|
||||
}
|
||||
|
||||
if (gameSessions.containsKey(realPlayerController.getId())) {
|
||||
setupTimeout(realPlayerController.getId());
|
||||
command.execute(realPlayerController.getId());
|
||||
}
|
||||
// TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case
|
||||
// same for another player (can be fixed by super-duper connection)
|
||||
if (informOthers) {
|
||||
informOthers(playerId);
|
||||
}
|
||||
} else {
|
||||
List<UUID> players = Splitter.split(game, playerId);
|
||||
for (UUID uuid : players) {
|
||||
if (gameSessions.containsKey(uuid)) {
|
||||
setupTimeout(uuid);
|
||||
command.execute(uuid);
|
||||
}
|
||||
}
|
||||
// TODO: if watcher disconnects then game freeze with active timer, must be fix for such use case
|
||||
// same for another player (can be fixed by super-duper connection)
|
||||
if (informOthers) {
|
||||
informOthers(players);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
package mage.server.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
/**
|
||||
* @author nantuko
|
||||
*/
|
||||
public final class Splitter {
|
||||
|
||||
private Splitter(){}
|
||||
|
||||
public static List<UUID> split(Game game, UUID playerId) {
|
||||
List<UUID> players = new ArrayList<>();
|
||||
//players.add(playerId); // add original player
|
||||
Player player = game.getPlayer(playerId);
|
||||
if (player != null && player.getTurnControlledBy() != null) {
|
||||
players.add(player.getTurnControlledBy());
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
|
@ -81,11 +81,8 @@ class DeceiverOfFormEffect extends OneShotEffect {
|
|||
&& ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard().isCreature(game)) {
|
||||
copyFromCard = ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard();
|
||||
}
|
||||
Permanent newBluePrint = null;
|
||||
newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game);
|
||||
newBluePrint.assignNewId();
|
||||
Permanent newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game);
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, permanent.getId());
|
||||
copyEffect.newId();
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
game.addEffect(copyEffect, newAbility);
|
||||
|
|
|
|||
|
|
@ -131,7 +131,6 @@ class DermotaxiCopyEffect extends OneShotEffect {
|
|||
DermotaxiCopyApplier applier = new DermotaxiCopyApplier();
|
||||
applier.apply(game, newBluePrint, source, sourcePermanent.getId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, sourcePermanent.getId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
game.addEffect(copyEffect, source);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -86,9 +86,8 @@ class DimirDoppelgangerEffect extends OneShotEffect {
|
|||
CopyApplier applier = new DimirDoppelgangerCopyApplier();
|
||||
applier.apply(game, newBluePrint, source, dimirDoppelganger.getId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, dimirDoppelganger.getId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
Ability newAbility = source.copy(); // TODO: why it copy new ability instead source? Some cards use it, some miss
|
||||
copyEffect.init(newAbility, game);
|
||||
game.addEffect(copyEffect, newAbility);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ class EchoingDeepsEffect extends OneShotEffect {
|
|||
CopyApplier applier = new EchoingDeepsApplier();
|
||||
applier.apply(game, newBluePrint, source, source.getSourceId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.WhileOnBattlefield, newBluePrint, source.getSourceId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
copyEffect.init(source, game);
|
||||
game.addEffect(copyEffect, source);
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ class IdentityThiefEffect extends OneShotEffect {
|
|||
ContinuousEffect copyEffect = new CopyEffect(Duration.EndOfTurn, targetPermanent, source.getSourceId());
|
||||
copyEffect.setTargetPointer(new FixedTarget(sourcePermanent.getId(), game));
|
||||
game.addEffect(copyEffect, source);
|
||||
|
||||
UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter());
|
||||
if (controller.moveCardsToExile(targetPermanent, source, game, true, exileZoneId, sourcePermanent.getName())) {
|
||||
Effect effect = new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, true);
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ class LazavDimirMastermindEffect extends OneShotEffect {
|
|||
CopyApplier applier = new LazavDimirMastermindCopyApplier();
|
||||
applier.apply(game, newBluePrint, source, lazavDimirMastermind.getId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavDimirMastermind.getId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,6 @@ class LazavTheMultifariousEffect extends OneShotEffect {
|
|||
CopyApplier applier = new LazavTheMultifariousCopyApplier();
|
||||
applier.apply(game, newBluePrint, source, lazavTheMultifarious.getId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavTheMultifarious.getId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ class LazotepConvertCopyEffect extends OneShotEffect {
|
|||
}
|
||||
Card modifiedCopy = copyFromCard.copy();
|
||||
//Appliers must be applied before CopyEffect, its applier setting is just for copies of copies
|
||||
// TODO: research applier usage, why it here
|
||||
applier.apply(game, modifiedCopy, source, source.getSourceId());
|
||||
game.addEffect(new CopyEffect(
|
||||
Duration.Custom, modifiedCopy, source.getSourceId()
|
||||
|
|
|
|||
|
|
@ -110,7 +110,6 @@ class LikenessLooterEffect extends OneShotEffect {
|
|||
CopyApplier applier = new LikenessLooterCopyApplier();
|
||||
applier.apply(game, newBluePrint, source, permanent.getId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, permanent.getId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ class OlagLudevicsHubrisEffect extends ReplacementEffectImpl {
|
|||
CopyApplier applier = new OlagLudevicsHubrisCopyApplier();
|
||||
applier.apply(game, newBluePrint, source, source.getSourceId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, source.getSourceId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ class ShadowKinEffect extends OneShotEffect {
|
|||
CopyApplier applier = new ShadowKinApplier();
|
||||
applier.apply(game, blueprint, source, sourcePermanent.getId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.Custom, blueprint, sourcePermanent.getId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ class TheMyriadPoolsCopyEffect extends OneShotEffect {
|
|||
newBluePrint = new PermanentCard(copyFromCardOnStack, source.getControllerId(), game);
|
||||
newBluePrint.assignNewId();
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, targetPermanentToCopyTo.getId());
|
||||
copyEffect.newId();
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
game.addEffect(copyEffect, newAbility);
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ class VolrathTheShapestealerEffect extends OneShotEffect {
|
|||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
Permanent volrathTheShapestealer = game.getPermanent(source.getSourceId());
|
||||
Permanent newBluePrint = null;
|
||||
if (controller == null
|
||||
|| volrathTheShapestealer == null) {
|
||||
return false;
|
||||
|
|
@ -100,12 +99,12 @@ class VolrathTheShapestealerEffect extends OneShotEffect {
|
|||
if (copyFromCard == null) {
|
||||
return true;
|
||||
}
|
||||
newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game);
|
||||
//newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game);
|
||||
Card newBluePrint = copyFromCard.copy();
|
||||
newBluePrint.assignNewId();
|
||||
CopyApplier applier = new VolrathTheShapestealerCopyApplier();
|
||||
applier.apply(game, newBluePrint, source, volrathTheShapestealer.getId());
|
||||
CopyEffect copyEffect = new CopyEffect(Duration.UntilYourNextTurn, newBluePrint, volrathTheShapestealer.getId());
|
||||
copyEffect.newId();
|
||||
copyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
copyEffect.init(newAbility, game);
|
||||
|
|
|
|||
|
|
@ -889,9 +889,10 @@ public class CopySpellTest extends CardTestPlayerBase {
|
|||
}
|
||||
|
||||
private void prepareZoneAndZCC(Card originalCard) {
|
||||
// prepare custom zcc and zone for copy testing
|
||||
// prepare custom zcc and zone for copy testing (it's not real game)
|
||||
// HAND zone is safest way (some card types require diff zones for stack/battlefield, e.g. MDFC)
|
||||
originalCard.setZoneChangeCounter(5, currentGame);
|
||||
originalCard.setZone(Zone.STACK, currentGame);
|
||||
originalCard.setZone(Zone.HAND, currentGame);
|
||||
}
|
||||
|
||||
private void cardsMustHaveSameZoneAndZCC(Card originalCard, Card copiedCard, String infoPrefix) {
|
||||
|
|
|
|||
|
|
@ -999,4 +999,33 @@ public class ModalDoubleFacedCardsTest extends CardTestPlayerBase {
|
|||
setStopAt(2, PhaseStep.END_TURN);
|
||||
execute();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Battlefield_MustHaveAbilitiesFromOneSideOnly() {
|
||||
// possible bug: test framework adds second side abilities
|
||||
|
||||
// left side - Reidane, God of the Worthy:
|
||||
// Snow lands your opponents control enter the battlefield tapped.
|
||||
// right side - Valkmira, Protector's Shield:
|
||||
// If a source an opponent controls would deal damage to you or a permanent you control, prevent 1 of that damage.
|
||||
// Whenever you or another permanent you control becomes the target of a spell or ability an opponent controls,
|
||||
// counter that spell or ability unless its controller pays {1}.
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Reidane, God of the Worthy", 1);
|
||||
//
|
||||
addCard(Zone.HAND, playerA, "Snow-Covered Forest", 1);
|
||||
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
|
||||
|
||||
// cast, second side effects must be ignored (e.g. counter trigger)
|
||||
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snow-Covered Forest");
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt");
|
||||
addTarget(playerA, playerB);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertTappedCount("Snow-Covered Forest", true, 1);
|
||||
assertLife(playerB, 20 - 3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import mage.cards.repository.CardRepository;
|
|||
import mage.constants.*;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.PutToBattlefieldInfo;
|
||||
import mage.game.match.Match;
|
||||
import mage.game.match.MatchType;
|
||||
import mage.game.permanent.PermanentCard;
|
||||
|
|
@ -72,7 +73,7 @@ public abstract class MageTestPlayerBase {
|
|||
protected Pattern pattern = Pattern.compile("([a-zA-Z]*):([\\w]*):([a-zA-Z ,\\-.!'\\d]*):([\\d]*)(:\\{tapped\\})?");
|
||||
|
||||
protected Map<TestPlayer, List<Card>> handCards = new HashMap<>();
|
||||
protected Map<TestPlayer, List<PermanentCard>> battlefieldCards = new HashMap<>();
|
||||
protected Map<TestPlayer, List<PutToBattlefieldInfo>> battlefieldCards = new HashMap<>(); // cards + additional status like tapped
|
||||
protected Map<TestPlayer, List<Card>> graveyardCards = new HashMap<>();
|
||||
protected Map<TestPlayer, List<Card>> libraryCards = new HashMap<>();
|
||||
protected Map<TestPlayer, List<Card>> commandCards = new HashMap<>();
|
||||
|
|
@ -111,7 +112,7 @@ public abstract class MageTestPlayerBase {
|
|||
EXPECTED
|
||||
}
|
||||
|
||||
protected ParserState parserState;
|
||||
protected ParserState parserState; // TODO: remove outdated and unsued code
|
||||
|
||||
/**
|
||||
* Expected results of the test. Read from test case in {@link String} based
|
||||
|
|
@ -216,6 +217,7 @@ public abstract class MageTestPlayerBase {
|
|||
}
|
||||
|
||||
private void parseLine(String line) {
|
||||
// TODO: delete unused code
|
||||
if (parserState == ParserState.EXPECTED) {
|
||||
expectedResults.add(line); // just remember for future use
|
||||
return;
|
||||
|
|
@ -229,14 +231,14 @@ public abstract class MageTestPlayerBase {
|
|||
|
||||
if (nickname.startsWith("Computer")) {
|
||||
List<Card> cards = null;
|
||||
List<PermanentCard> perms = null;
|
||||
//List<PermanentCard> perms = null;
|
||||
Zone gameZone;
|
||||
if ("hand".equalsIgnoreCase(zone)) {
|
||||
gameZone = Zone.HAND;
|
||||
cards = getHandCards(getPlayer(nickname));
|
||||
} else if ("battlefield".equalsIgnoreCase(zone)) {
|
||||
gameZone = Zone.BATTLEFIELD;
|
||||
perms = getBattlefieldCards(getPlayer(nickname));
|
||||
//perms = getBattlefieldCards(getPlayer(nickname));
|
||||
} else if ("graveyard".equalsIgnoreCase(zone)) {
|
||||
gameZone = Zone.GRAVEYARD;
|
||||
cards = getGraveCards(getPlayer(nickname));
|
||||
|
|
@ -268,10 +270,10 @@ public abstract class MageTestPlayerBase {
|
|||
Card newCard = cardInfo != null ? cardInfo.getCard() : null;
|
||||
if (newCard != null) {
|
||||
if (gameZone == Zone.BATTLEFIELD) {
|
||||
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
|
||||
PermanentCard p = new PermanentCard(permCard, null, currentGame);
|
||||
p.setTapped(tapped);
|
||||
perms.add(p);
|
||||
//Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
|
||||
//PermanentCard p = new PermanentCard(permCard, null, currentGame);
|
||||
//p.setTapped(tapped);
|
||||
//perms.add(p);
|
||||
} else {
|
||||
cards.add(newCard);
|
||||
}
|
||||
|
|
@ -339,11 +341,11 @@ public abstract class MageTestPlayerBase {
|
|||
return res;
|
||||
}
|
||||
|
||||
protected List<PermanentCard> getBattlefieldCards(TestPlayer player) {
|
||||
protected List<PutToBattlefieldInfo> getBattlefieldCards(TestPlayer player) {
|
||||
if (battlefieldCards.containsKey(player)) {
|
||||
return battlefieldCards.get(player);
|
||||
}
|
||||
List<PermanentCard> res = new ArrayList<>();
|
||||
List<PutToBattlefieldInfo> res = new ArrayList<>();
|
||||
battlefieldCards.put(player, res);
|
||||
return res;
|
||||
}
|
||||
|
|
@ -437,12 +439,13 @@ public abstract class MageTestPlayerBase {
|
|||
|
||||
CardSetInfo testSet = new CardSetInfo(needCardName, needSetCode, "123", Rarity.COMMON);
|
||||
Card newCard = new CustomTestCard(controllerPlayer.getId(), testSet, cardType, spellCost);
|
||||
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
|
||||
PermanentCard permanent = new PermanentCard(permCard, controllerPlayer.getId(), currentGame);
|
||||
|
||||
switch (putAtZone) {
|
||||
case BATTLEFIELD:
|
||||
getBattlefieldCards(controllerPlayer).add(permanent);
|
||||
getBattlefieldCards(controllerPlayer).add(new PutToBattlefieldInfo(
|
||||
newCard,
|
||||
false
|
||||
));
|
||||
break;
|
||||
case GRAVEYARD:
|
||||
getGraveCards(controllerPlayer).add(newCard);
|
||||
|
|
@ -565,7 +568,7 @@ public abstract class MageTestPlayerBase {
|
|||
}
|
||||
}
|
||||
|
||||
// custom card with global abilities list to init (can contains abilities per card name)
|
||||
// custom card with global abilities list to init (can contain abilities per card name)
|
||||
class CustomTestCard extends CardImpl {
|
||||
|
||||
static private final Map<String, Abilities<Ability>> abilitiesList = new HashMap<>(); // card name -> abilities
|
||||
|
|
|
|||
|
|
@ -631,7 +631,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
|
|||
}
|
||||
|
||||
/**
|
||||
* Add any amount of cards to specified zone of specified player.
|
||||
* Add any amount of cards to specified zone of specified player without resolve/ETB
|
||||
*
|
||||
* @param gameZone {@link mage.constants.Zone} to add cards to.
|
||||
* @param player {@link Player} to add cards for. Use either playerA or
|
||||
|
|
@ -684,14 +684,13 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
|
|||
if (gameZone == Zone.BATTLEFIELD) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
Card newCard = cardInfo.getCard();
|
||||
Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard);
|
||||
|
||||
PermanentCard p = new PermanentCard(permCard, player.getId(), currentGame);
|
||||
p.setTapped(tapped);
|
||||
getBattlefieldCards(player).add(p);
|
||||
|
||||
getBattlefieldCards(player).add(new PutToBattlefieldInfo(
|
||||
newCard,
|
||||
tapped
|
||||
));
|
||||
if (!aliasName.isEmpty()) {
|
||||
player.addAlias(player.generateAliasName(aliasName, useAliasMultiNames, i + 1), p.getId());
|
||||
// TODO: is it bugged with double faced cards (wrong ref)?
|
||||
player.addAlias(player.generateAliasName(aliasName, useAliasMultiNames, i + 1), newCard.getId());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -78,14 +78,16 @@ public class AddCardApiTest extends CardTestPlayerBase {
|
|||
execute();
|
||||
|
||||
assertPermanentCount(playerA, "Memorial to Glory", 2);
|
||||
getBattlefieldCards(playerA).stream()
|
||||
.filter(card -> card.getName().equals("Memorial to Glory"))
|
||||
.forEach(card -> Assert.assertEquals("40K", card.getExpansionSetCode()));
|
||||
getBattlefieldCards(playerA)
|
||||
.stream()
|
||||
.filter(info -> info.getCard().getName().equals("Memorial to Glory"))
|
||||
.forEach(info -> Assert.assertEquals("40K", info.getCard().getExpansionSetCode()));
|
||||
|
||||
assertPermanentCount(playerA, "Plains", 2);
|
||||
getBattlefieldCards(playerA).stream()
|
||||
.filter(card -> card.getName().equals("Plains"))
|
||||
.forEach(card -> Assert.assertEquals("PANA", card.getExpansionSetCode()));
|
||||
getBattlefieldCards(playerA)
|
||||
.stream()
|
||||
.filter(info -> info.getCard().getName().equals("Plains"))
|
||||
.forEach(info -> Assert.assertEquals("PANA", info.getCard().getExpansionSetCode()));
|
||||
}
|
||||
|
||||
@Test(expected = org.junit.ComparisonFailure.class)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public class ExtraTurnsTest extends CardTestPlayerBase {
|
|||
private void checkTurnControl(int turn, TestPlayer needTurnController, boolean isExtraTurn) {
|
||||
runCode("checking turn " + turn, turn, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
|
||||
Player defaultTurnController = game.getPlayer(game.getActivePlayerId());
|
||||
Player realTurnController = defaultTurnController.getTurnControlledBy() == null ? defaultTurnController : game.getPlayer(defaultTurnController.getTurnControlledBy());
|
||||
Player realTurnController = game.getPlayer(defaultTurnController.getTurnControlledBy());
|
||||
Assert.assertEquals(String.format("turn %d must be controlled by %s", turn, needTurnController.getName()),
|
||||
needTurnController.getName(), realTurnController.getName());
|
||||
Assert.assertEquals(String.format("turn %d must be %s", turn, (isExtraTurn ? "extra turn" : "normal turn")),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import mage.game.Game;
|
|||
import mage.players.Player;
|
||||
|
||||
/**
|
||||
* TODO: delete, there are already end turn code with control reset
|
||||
* @author nantuko
|
||||
*/
|
||||
public class LoseControlOnOtherPlayersControllerEffect extends OneShotEffect {
|
||||
|
|
|
|||
|
|
@ -94,15 +94,29 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
|
|||
@Override
|
||||
public void setCopy(boolean isCopy, MageObject copiedFrom) {
|
||||
super.setCopy(isCopy, copiedFrom);
|
||||
leftHalfCard.setCopy(isCopy, copiedFrom);
|
||||
leftHalfCard.setCopy(isCopy, copiedFrom); // TODO: must check copiedFrom and assign sides? (??? related to #8476 ???)
|
||||
rightHalfCard.setCopy(isCopy, copiedFrom);
|
||||
}
|
||||
|
||||
private void setSideZones(Zone mainZone, Game game) {
|
||||
switch (mainZone) {
|
||||
case BATTLEFIELD:
|
||||
case STACK:
|
||||
throw new IllegalArgumentException("Wrong code usage: you must put to battlefield/stack only real side card (half), not main");
|
||||
default:
|
||||
// must keep both sides in same zone cause xmage need access to cost reduction, spell
|
||||
// and other abilities before put it to stack (in playable calcs)
|
||||
game.setZone(leftHalfCard.getId(), mainZone);
|
||||
game.setZone(rightHalfCard.getId(), mainZone);
|
||||
break;
|
||||
}
|
||||
checkGoodZones(game, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
|
||||
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
|
||||
game.getState().setZone(leftHalfCard.getId(), toZone);
|
||||
game.getState().setZone(rightHalfCard.getId(), toZone);
|
||||
setSideZones(toZone, game);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -111,21 +125,69 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
|
|||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
super.setZone(zone, game);
|
||||
game.setZone(leftHalfCard.getId(), zone);
|
||||
game.setZone(rightHalfCard.getId(), zone);
|
||||
setSideZones(zone, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
|
||||
if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
|
||||
Zone currentZone = game.getState().getZone(getId());
|
||||
game.getState().setZone(leftHalfCard.getId(), currentZone);
|
||||
game.getState().setZone(rightHalfCard.getId(), currentZone);
|
||||
setSideZones(Zone.EXILED, game);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime check for good zones and other MDF data
|
||||
*/
|
||||
public static void checkGoodZones(Game game, ModalDoubleFacedCard card) {
|
||||
Card leftPart = card.getLeftHalfCard();
|
||||
Card rightPart = card.getRightHalfCard();
|
||||
|
||||
Zone zoneMain = game.getState().getZone(card.getId());
|
||||
Zone zoneLeft = game.getState().getZone(leftPart.getId());
|
||||
Zone zoneRight = game.getState().getZone(rightPart.getId());
|
||||
|
||||
// runtime check:
|
||||
// * in battlefield and stack - card + one of the sides (another side in outside zone)
|
||||
// * in other zones - card + both sides (need both sides due cost reductions, spell and other access before put to stack)
|
||||
//
|
||||
// 712.8a While a double-faced card is outside the game or in a zone other than the battlefield or stack,
|
||||
// it has only the characteristics of its front face.
|
||||
//
|
||||
// 712.8f While a modal double-faced spell is on the stack or a modal double-faced permanent is on the battlefield,
|
||||
// it has only the characteristics of the face that’s up.
|
||||
Zone needZoneLeft;
|
||||
Zone needZoneRight;
|
||||
switch (zoneMain) {
|
||||
case BATTLEFIELD:
|
||||
case STACK:
|
||||
if (zoneMain == zoneLeft) {
|
||||
needZoneLeft = zoneMain;
|
||||
needZoneRight = Zone.OUTSIDE;
|
||||
} else if (zoneMain == zoneRight) {
|
||||
needZoneLeft = Zone.OUTSIDE;
|
||||
needZoneRight = zoneMain;
|
||||
} else {
|
||||
// impossible
|
||||
needZoneLeft = zoneMain;
|
||||
needZoneRight = Zone.OUTSIDE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
needZoneLeft = zoneMain;
|
||||
needZoneRight = zoneMain;
|
||||
break;
|
||||
}
|
||||
|
||||
if (zoneLeft != needZoneLeft || zoneRight != needZoneRight) {
|
||||
throw new IllegalStateException("Wrong code usage: MDF card uses wrong zones - " + card
|
||||
+ "\r\n" + String.format("* main zone: %s", zoneMain)
|
||||
+ "\r\n" + String.format("* left side: need %s, actual %s", needZoneLeft, zoneLeft)
|
||||
+ "\r\n" + String.format("* right side: need %s, actual %s", needZoneRight, zoneRight));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
|
||||
// zone contains only one main card
|
||||
|
|
|
|||
|
|
@ -71,9 +71,32 @@ public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubl
|
|||
|
||||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
// see ModalDoubleFacedCard.checkGoodZones for details
|
||||
game.setZone(parentCard.getId(), zone);
|
||||
game.setZone(parentCard.getLeftHalfCard().getId(), zone);
|
||||
game.setZone(parentCard.getRightHalfCard().getId(), zone);
|
||||
game.setZone(this.getId(), zone);
|
||||
|
||||
// find another side to sync
|
||||
ModalDoubleFacedCardHalf otherSide;
|
||||
if (!parentCard.getLeftHalfCard().getId().equals(this.getId())) {
|
||||
otherSide = parentCard.getLeftHalfCard();
|
||||
} else if (!parentCard.getRightHalfCard().getId().equals(this.getId())) {
|
||||
otherSide = parentCard.getRightHalfCard();
|
||||
} else {
|
||||
throw new IllegalStateException("Wrong code usage: MDF halves must use different ids");
|
||||
}
|
||||
|
||||
switch (zone) {
|
||||
case STACK:
|
||||
case BATTLEFIELD:
|
||||
// stack and battlefield must have only one side
|
||||
game.setZone(otherSide.getId(), Zone.OUTSIDE);
|
||||
break;
|
||||
default:
|
||||
game.setZone(otherSide.getId(), zone);
|
||||
break;
|
||||
}
|
||||
|
||||
ModalDoubleFacedCard.checkGoodZones(game, parentCard);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
|||
|
||||
// game cheats (for tests only)
|
||||
void cheat(UUID ownerId, Map<Zone, String> commands);
|
||||
void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled);
|
||||
void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PutToBattlefieldInfo> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled);
|
||||
|
||||
// controlling the behaviour of replacement effects while permanents entering the battlefield
|
||||
void setScopeRelevant(boolean scopeRelevant);
|
||||
|
|
|
|||
|
|
@ -288,6 +288,8 @@ public abstract class GameImpl implements Game {
|
|||
public void loadCards(Set<Card> cards, UUID ownerId) {
|
||||
for (Card card : cards) {
|
||||
if (card instanceof PermanentCard) {
|
||||
// TODO: impossible use case, can be deleted?
|
||||
// trying to put permanent card to battlefield
|
||||
card = ((PermanentCard) card).getCard();
|
||||
}
|
||||
|
||||
|
|
@ -2019,11 +2021,10 @@ public abstract class GameImpl implements Game {
|
|||
// save original copy link (handle copy of copies too)
|
||||
newBluePrint.setCopy(true, (copyFromPermanent.getCopyFrom() != null ? copyFromPermanent.getCopyFrom() : copyFromPermanent));
|
||||
|
||||
CopyEffect newEffect = new CopyEffect(duration, newBluePrint, copyToPermanentId);
|
||||
newEffect.newId();
|
||||
newEffect.setApplier(applier);
|
||||
CopyEffect newCopyEffect = new CopyEffect(duration, newBluePrint, copyToPermanentId);
|
||||
newCopyEffect.setApplier(applier);
|
||||
Ability newAbility = source.copy();
|
||||
newEffect.init(newAbility, this);
|
||||
newCopyEffect.init(newAbility, this);
|
||||
|
||||
// If there are already copy effects with duration = Custom to the same object, remove the existing effects because they no longer have any effect
|
||||
if (duration == Duration.Custom) {
|
||||
|
|
@ -2037,7 +2038,7 @@ public abstract class GameImpl implements Game {
|
|||
}
|
||||
}
|
||||
}
|
||||
state.addEffect(newEffect, newAbility);
|
||||
state.addEffect(newCopyEffect, newAbility);
|
||||
return newBluePrint;
|
||||
}
|
||||
|
||||
|
|
@ -3567,20 +3568,27 @@ public abstract class GameImpl implements Game {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled) {
|
||||
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PutToBattlefieldInfo> battlefield, List<Card> graveyard, List<Card> command, List<Card> exiled) {
|
||||
// fake test ability for triggers and events
|
||||
Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards"));
|
||||
fakeSourceAbilityTemplate.setControllerId(ownerId);
|
||||
|
||||
Player player = getPlayer(ownerId);
|
||||
if (player != null) {
|
||||
// init cards
|
||||
loadCards(ownerId, library);
|
||||
loadCards(ownerId, hand);
|
||||
loadCards(ownerId, battlefield);
|
||||
loadCards(ownerId, battlefield
|
||||
.stream()
|
||||
.map(PutToBattlefieldInfo::getCard)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
loadCards(ownerId, graveyard);
|
||||
loadCards(ownerId, command);
|
||||
loadCards(ownerId, exiled);
|
||||
|
||||
// move cards to zones
|
||||
|
||||
for (Card card : library) {
|
||||
player.getLibrary().putOnTop(card, this);
|
||||
}
|
||||
|
|
@ -3610,10 +3618,10 @@ public abstract class GameImpl implements Game {
|
|||
getExile().add(card);
|
||||
}
|
||||
|
||||
for (PermanentCard permanentCard : battlefield) {
|
||||
for (PutToBattlefieldInfo info : battlefield) {
|
||||
Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy();
|
||||
fakeSourceAbility.setSourceId(permanentCard.getId());
|
||||
CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, permanentCard, player);
|
||||
fakeSourceAbility.setSourceId(info.getCard().getId());
|
||||
CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, info.getCard(), player, info.isTapped());
|
||||
}
|
||||
|
||||
applyEffects();
|
||||
|
|
|
|||
27
Mage/src/main/java/mage/game/PutToBattlefieldInfo.java
Normal file
27
Mage/src/main/java/mage/game/PutToBattlefieldInfo.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package mage.game;
|
||||
|
||||
import mage.cards.Card;
|
||||
|
||||
/**
|
||||
* For tests only: put to battlefield with additional settings like tapped
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class PutToBattlefieldInfo {
|
||||
|
||||
private final Card card;
|
||||
private final boolean tapped;
|
||||
|
||||
public PutToBattlefieldInfo(Card card, boolean tapped) {
|
||||
this.card = card;
|
||||
this.tapped = tapped;
|
||||
}
|
||||
|
||||
public Card getCard() {
|
||||
return card;
|
||||
}
|
||||
|
||||
public boolean isTapped() {
|
||||
return tapped;
|
||||
}
|
||||
}
|
||||
|
|
@ -439,6 +439,7 @@ public final class ZonesHandler {
|
|||
if (Zone.STACK == event.getFromZone()) {
|
||||
Spell spell = game.getStack().getSpell(event.getTargetId());
|
||||
if (spell != null && !spell.isFaceDown(game)) {
|
||||
// TODO: wtf, why only colors!? Must research and remove colors workaround
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import mage.cards.SplitCard;
|
|||
import mage.constants.SpellAbilityType;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
import mage.game.permanent.token.Token;
|
||||
|
||||
import javax.annotation.processing.SupportedSourceVersion;
|
||||
import javax.lang.model.SourceVersion;
|
||||
|
|
@ -25,18 +26,23 @@ import java.util.UUID;
|
|||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_8)
|
||||
public class PermanentCard extends PermanentImpl {
|
||||
|
||||
protected int maxLevelCounters;
|
||||
// A copy of the origin card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype)
|
||||
protected Card card;
|
||||
// A copy of the original card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype)
|
||||
protected Card card; // TODO: wtf, it modified on getCard and other places, e.g. on bestow -- must be fixed!
|
||||
// the number this permanent instance had
|
||||
protected int zoneChangeCounter;
|
||||
|
||||
public PermanentCard(Card card, UUID controllerId, Game game) {
|
||||
super(card.getId(), card.getOwnerId(), controllerId, card.getName());
|
||||
|
||||
// runtime check: must use real card only inside
|
||||
if (card instanceof PermanentCard) {
|
||||
// TODO: allow?
|
||||
throw new IllegalArgumentException("Wrong code usage: can't use PermanentCard inside another PermanentCard");
|
||||
}
|
||||
|
||||
// usage check: you must put to play only real card's part
|
||||
// if you use it in test code then call CardUtil.getDefaultCardSideForBattlefield for default side
|
||||
// it's a basic check and still allows to create permanent from instant or sorcery
|
||||
|
|
@ -101,6 +107,7 @@ public class PermanentCard extends PermanentImpl {
|
|||
}
|
||||
|
||||
protected void copyFromCard(final Card card, final Game game) {
|
||||
// TODO: must research - is it copy all fields or something miss
|
||||
this.name = card.getName();
|
||||
this.abilities.clear();
|
||||
if (this.faceDown) {
|
||||
|
|
@ -224,6 +231,10 @@ public class PermanentCard extends PermanentImpl {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return card.toString();
|
||||
return card.toString()
|
||||
+ ", " + ((this instanceof Token) ? "T" : "C")
|
||||
+ (this.isCopy() ? ", copy" : "")
|
||||
+ ", " + this.getPower() + "/" + this.getToughness()
|
||||
+ (this.isTapped() ? ", tapped" : "");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,6 @@ public class SpellStack extends ArrayDeque<StackObject> {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.size() + (this.isEmpty() ? "" : " (top: " + CardUtil.substring(this.getFirst().toString(), 100) + ")");
|
||||
return this.size() + (this.isEmpty() ? "" : " (top: " + CardUtil.substring(this.getFirst().toString(), 100, "...") + ")");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,10 +294,8 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
void setTopCardRevealed(boolean topCardRevealed);
|
||||
|
||||
/**
|
||||
* Get data from the client Preferences (e.g. avatarId or
|
||||
* showAbilityPickerForce)
|
||||
*
|
||||
* @return
|
||||
* User's settings like avatar or skip buttons.
|
||||
* WARNING, game related code must use controlling player settings only, e.g. getControllingPlayersUserData
|
||||
*/
|
||||
UserData getUserData();
|
||||
|
||||
|
|
@ -333,6 +331,9 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
|
||||
List<UUID> getTurnControllers();
|
||||
|
||||
/**
|
||||
* Current turn controller for a player (return own id for own control)
|
||||
*/
|
||||
UUID getTurnControlledBy();
|
||||
|
||||
/**
|
||||
|
|
@ -360,6 +361,11 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
*/
|
||||
void setGameUnderYourControl(boolean value);
|
||||
|
||||
/**
|
||||
* Return player's turn control to prev player
|
||||
* @param value
|
||||
* @param fullRestore return turn control to own
|
||||
*/
|
||||
void setGameUnderYourControl(boolean value, boolean fullRestore);
|
||||
|
||||
void setTestMode(boolean value);
|
||||
|
|
|
|||
|
|
@ -159,10 +159,12 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
protected FilterPermanent sacrificeCostFilter;
|
||||
protected final List<AlternativeSourceCosts> alternativeSourceCosts = new ArrayList<>();
|
||||
|
||||
protected boolean isGameUnderControl = true;
|
||||
protected UUID turnController;
|
||||
protected List<UUID> turnControllers = new ArrayList<>();
|
||||
protected Set<UUID> playersUnderYourControl = new HashSet<>();
|
||||
// TODO: rework turn controller to use single list (see other todos)
|
||||
//protected Stack<UUID> allTurnControllers = new Stack<>();
|
||||
protected boolean isGameUnderControl = true; // TODO: replace with allTurnControllers.isEmpty
|
||||
protected UUID turnController; // null on own control TODO: replace with allTurnControllers.last
|
||||
protected List<UUID> turnControllers = new ArrayList<>(); // TODO: remove
|
||||
protected Set<UUID> playersUnderYourControl = new HashSet<>(); // TODO: replace with game method and search in allTurnControllers
|
||||
|
||||
protected Set<UUID> usersAllowedToSeeHandCards = new HashSet<>();
|
||||
protected List<UUID> attachments = new ArrayList<>();
|
||||
|
|
@ -263,14 +265,14 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.storedBookmark = player.storedBookmark;
|
||||
|
||||
this.topCardRevealed = player.topCardRevealed;
|
||||
this.playersUnderYourControl.addAll(player.playersUnderYourControl);
|
||||
this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards);
|
||||
|
||||
this.isTestMode = player.isTestMode;
|
||||
this.isGameUnderControl = player.isGameUnderControl;
|
||||
|
||||
this.isGameUnderControl = player.isGameUnderControl;
|
||||
this.turnController = player.turnController;
|
||||
this.turnControllers.addAll(player.turnControllers);
|
||||
this.playersUnderYourControl.addAll(player.playersUnderYourControl);
|
||||
|
||||
this.passed = player.passed;
|
||||
this.passedTurn = player.passedTurn;
|
||||
|
|
@ -363,13 +365,14 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts());
|
||||
|
||||
this.topCardRevealed = player.isTopCardRevealed();
|
||||
this.playersUnderYourControl.clear();
|
||||
this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl());
|
||||
this.isGameUnderControl = player.isGameUnderControl();
|
||||
|
||||
this.turnController = player.getTurnControlledBy();
|
||||
this.isGameUnderControl = player.isGameUnderControl();
|
||||
this.turnController = this.getId().equals(player.getTurnControlledBy()) ? null : player.getTurnControlledBy();
|
||||
this.turnControllers.clear();
|
||||
this.turnControllers.addAll(player.getTurnControllers());
|
||||
this.playersUnderYourControl.clear();
|
||||
this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl());
|
||||
|
||||
this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving();
|
||||
|
||||
this.clearCastSourceIdManaCosts();
|
||||
|
|
@ -607,6 +610,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
|
||||
@Override
|
||||
public void setTurnControlledBy(UUID playerId) {
|
||||
if (playerId == null) {
|
||||
throw new IllegalArgumentException("Can't add unknown player to turn controllers: " + playerId);
|
||||
}
|
||||
this.turnController = playerId;
|
||||
this.turnControllers.add(playerId);
|
||||
}
|
||||
|
|
@ -618,7 +624,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
|
||||
@Override
|
||||
public UUID getTurnControlledBy() {
|
||||
return this.turnController;
|
||||
return this.turnController == null ? this.getId() : this.turnController;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -647,14 +653,18 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.isGameUnderControl = value;
|
||||
if (isGameUnderControl) {
|
||||
if (fullRestore) {
|
||||
// to own
|
||||
this.turnControllers.clear();
|
||||
this.turnController = getId();
|
||||
this.turnController = null;
|
||||
this.isGameUnderControl = true;
|
||||
} else {
|
||||
// to prev player
|
||||
if (!turnControllers.isEmpty()) {
|
||||
this.turnControllers.remove(turnControllers.size() - 1);
|
||||
}
|
||||
if (turnControllers.isEmpty()) {
|
||||
this.turnController = getId();
|
||||
this.turnController = null;
|
||||
this.isGameUnderControl = true;
|
||||
} else {
|
||||
this.turnController = turnControllers.get(turnControllers.size() - 1);
|
||||
isGameUnderControl = false;
|
||||
|
|
@ -2515,7 +2525,8 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
@Override
|
||||
public void concede(Game game) {
|
||||
game.setConcedingPlayer(playerId);
|
||||
lost(game);
|
||||
|
||||
lost(game); // it's ok to be ignored by "can't lose abilities" here (setConcedingPlayer done all work above)
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1082,16 +1082,26 @@ public final class CardUtil {
|
|||
}
|
||||
|
||||
/**
|
||||
* Put card to battlefield without resolve (for cheats and tests only)
|
||||
* Put card to battlefield without resolve/ETB (for cheats and tests only)
|
||||
*
|
||||
* @param source must be non null (if you need it empty then use fakeSourceAbility)
|
||||
* @param source must be non-null (if you need it empty then use fakeSourceAbility)
|
||||
* @param game
|
||||
* @param newCard
|
||||
* @param player
|
||||
*/
|
||||
public static void putCardOntoBattlefieldWithEffects(Ability source, Game game, Card newCard, Player player) {
|
||||
public static void putCardOntoBattlefieldWithEffects(Ability source, Game game, Card newCard, Player player, boolean tapped) {
|
||||
// same logic as ZonesHandler->maybeRemoveFromSourceZone
|
||||
|
||||
// runtime check: must have source
|
||||
if (source == null) {
|
||||
throw new IllegalArgumentException("Wrong code usage: must use source ability or fakeSourceAbility");
|
||||
}
|
||||
|
||||
// runtime check: must use only real cards
|
||||
if (newCard instanceof PermanentCard) {
|
||||
throw new IllegalArgumentException("Wrong code usage: must put to battlefield only real cards, not PermanentCard");
|
||||
}
|
||||
|
||||
// workaround to put real permanent from one side (example: you call mdf card by cheats)
|
||||
Card permCard = getDefaultCardSideForBattlefield(game, newCard);
|
||||
|
||||
|
|
@ -1105,15 +1115,16 @@ public final class CardUtil {
|
|||
permanent = new PermanentCard(permCard, player.getId(), game);
|
||||
}
|
||||
|
||||
// put onto battlefield with possible counters
|
||||
// put onto battlefield with possible counters without ETB
|
||||
game.getPermanentsEntering().put(permanent.getId(), permanent);
|
||||
permCard.checkForCountersToAdd(permanent, source, game);
|
||||
permanent.entersBattlefield(source, game, Zone.OUTSIDE, false);
|
||||
game.addPermanent(permanent, game.getState().getNextPermanentOrderNumber());
|
||||
game.getPermanentsEntering().remove(permanent.getId());
|
||||
|
||||
// workaround for special tapped status from test framework's command (addCard)
|
||||
if (permCard instanceof PermanentCard && ((PermanentCard) permCard).isTapped()) {
|
||||
// tapped status
|
||||
// warning, "enters the battlefield tapped" abilities will be executed before, so don't set to false here
|
||||
if (tapped) {
|
||||
permanent.setTapped(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue