├── .gitignore ├── .travis.yml ├── stubby ├── json │ ├── types │ │ ├── supertypes.json │ │ ├── types.json │ │ └── subtypes.json │ ├── sets │ │ ├── DRK.json │ │ ├── LEA.json │ │ └── LEB.json │ └── cards │ │ ├── GiftsGiven.json │ │ ├── 10.json │ │ ├── 926234c2fe8863f49220a878346c4c5ca79b6046.json │ │ ├── 1.json │ │ ├── 8a5d85644f546525433c4472b76c3b0ebb495b33.json │ │ ├── ffa00e95-754e-5484-8e4c-e3b707d4c1d2.json │ │ ├── NicolBolasTheArisen.json │ │ └── DRKpage2.json └── stubby-config.yaml ├── stubby-config.yaml ├── src ├── main │ └── java │ │ └── io │ │ └── magicthegathering │ │ └── javasdk │ │ ├── exception │ │ └── HttpRequestFailedException.java │ │ ├── resource │ │ ├── Legality.java │ │ ├── Ruling.java │ │ ├── ForeignData.java │ │ ├── MtgSet.java │ │ └── Card.java │ │ └── api │ │ ├── CardAPI.java │ │ ├── SetAPI.java │ │ └── MTGAPI.java └── test │ └── java │ └── io │ └── magicthegathering │ └── javasdk │ └── api │ ├── MTGAPITest.java │ ├── SetAPITest.java │ └── CardAPITest.java ├── LICENSE ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | 3 | /.settings/ 4 | .project 5 | .classpath 6 | settings.xml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | dist: trusty 4 | 5 | jdk: openjdk7 6 | 7 | install: mvn install -DskipTests -Dgpg.skip 8 | -------------------------------------------------------------------------------- /stubby/json/types/supertypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "supertypes": [ 3 | "Basic", 4 | "Legendary", 5 | "Ongoing", 6 | "Snow", 7 | "World" 8 | ] 9 | } -------------------------------------------------------------------------------- /stubby-config.yaml: -------------------------------------------------------------------------------- 1 | - request: 2 | url: /cards/-1 3 | method: GET 4 | response: 5 | headers: 6 | Content-Type: application/json 7 | latency: 1000 8 | status: 200 9 | body: -------------------------------------------------------------------------------- /stubby/json/types/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | "Artifact", 4 | "Conspiracy", 5 | "Creature", 6 | "Enchantment", 7 | "Instant", 8 | "Land", 9 | "Phenomenon", 10 | "Plane", 11 | "Planeswalker", 12 | "Scheme", 13 | "Sorcery", 14 | "Tribal", 15 | "Vanguard" 16 | ] 17 | } -------------------------------------------------------------------------------- /stubby/json/sets/DRK.json: -------------------------------------------------------------------------------- 1 | { 2 | "set": { 3 | "code": "DRK", 4 | "name": "The Dark", 5 | "type": "expansion", 6 | "border": "black", 7 | "mkm_id": 8, 8 | "booster": [ 9 | "uncommon", 10 | "uncommon", 11 | "common", 12 | "common", 13 | "common", 14 | "common", 15 | "common", 16 | "common" 17 | ], 18 | "mkm_name": "The Dark", 19 | "releaseDate": "1994-08-08", 20 | "gathererCode": "DK", 21 | "magicCardsInfoCode": "dk" 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/exception/HttpRequestFailedException.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.exception; 2 | 3 | import io.magicthegathering.javasdk.api.MTGAPI; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * Thrown by {@link MTGAPI} when an http request to magicthegathering.io API fails to return anything. 9 | * 10 | * @author nniklas 11 | * 12 | */ 13 | public class HttpRequestFailedException extends RuntimeException { 14 | 15 | public HttpRequestFailedException(IOException e) { 16 | super(e); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /stubby/json/sets/LEA.json: -------------------------------------------------------------------------------- 1 | { 2 | "set": { 3 | "code": "LEA", 4 | "name": "Limited Edition Alpha", 5 | "type": "core", 6 | "border": "black", 7 | "mkm_id": 1, 8 | "booster": [ 9 | "rare", 10 | "uncommon", 11 | "uncommon", 12 | "uncommon", 13 | "common", 14 | "common", 15 | "common", 16 | "common", 17 | "common", 18 | "common", 19 | "common", 20 | "common", 21 | "common", 22 | "common", 23 | "common" 24 | ], 25 | "mkm_name": "Alpha", 26 | "releaseDate": "1993-08-05", 27 | "gathererCode": "1E", 28 | "magicCardsInfoCode": "al" 29 | } 30 | } -------------------------------------------------------------------------------- /stubby/json/sets/LEB.json: -------------------------------------------------------------------------------- 1 | { 2 | "set": { 3 | "code": "LEB", 4 | "name": "Limited Edition Beta", 5 | "type": "core", 6 | "border": "black", 7 | "mkm_id": 2, 8 | "booster": [ 9 | "rare", 10 | "uncommon", 11 | "uncommon", 12 | "uncommon", 13 | "common", 14 | "common", 15 | "common", 16 | "common", 17 | "common", 18 | "common", 19 | "common", 20 | "common", 21 | "common", 22 | "common", 23 | "common" 24 | ], 25 | "mkm_name": "Beta", 26 | "releaseDate": "1993-10-01", 27 | "gathererCode": "2E", 28 | "magicCardsInfoCode": "be" 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/java/io/magicthegathering/javasdk/api/MTGAPITest.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.api; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | 6 | import by.stub.client.StubbyClient; 7 | 8 | /** 9 | * Base class for unit tests. Takes care of starting and stopping a stubby4j 10 | * server to simulate the magicthegathering.io REST APIs locally. 11 | * 12 | * @author nniklas 13 | * 14 | */ 15 | public class MTGAPITest { 16 | private StubbyClient stubbyClient; 17 | 18 | @Before 19 | public void initStubby() throws Exception { 20 | stubbyClient = new StubbyClient(); 21 | stubbyClient.startJetty("stubby/stubby-config.yaml"); 22 | MTGAPI.ENDPOINT = "http://localhost:8882"; 23 | } 24 | 25 | @After 26 | public void teardownStubby() throws Exception { 27 | stubbyClient.stopJetty(); 28 | // TODO Remove when all tests use stubby instead of real http calls 29 | MTGAPI.ENDPOINT = "https://api.magicthegathering.io/v1"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /stubby/json/cards/GiftsGiven.json: -------------------------------------------------------------------------------- 1 | { 2 | "cards": [ 3 | { 4 | "name": "Gifts Given", 5 | "manaCost": "{3}{U}", 6 | "cmc": 4, 7 | "colors": [ 8 | "Blue" 9 | ], 10 | "colorIdentity": [ 11 | "U" 12 | ], 13 | "type": "Instant", 14 | "types": [ 15 | "Instant" 16 | ], 17 | "rarity": "Special", 18 | "set": "pHHO", 19 | "setName": "Happy Holidays", 20 | "text": "Search target opponent's library for four cards with different names and reveal them. That player chooses two of those cards. Put the chosen cards into the player's graveyard and the rest into your hand. Then that player shuffles his or her library.", 21 | "flavor": "\"Thanks! You shouldn't have.\"", 22 | "artist": "Jason Chan", 23 | "number": "2", 24 | "layout": "normal", 25 | "releaseDate": "2007", 26 | "printings": [ 27 | "pHHO" 28 | ], 29 | "source": "Holiday gift to Wizards internal teams and business partners.", 30 | "id": "689dd21260c8e01942e248e560ee3928ee56aa77" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 magicthegathering.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/resource/Legality.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.resource; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * This file is part of mtgsdk. 7 | * https://github.com/MagicTheGathering/mtg-sdk-java 8 | *

9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT-license 11 | *

12 | * Created by thechucklingatom on 5/8/2017. 13 | *

14 | * The Legality sub object in the card json object. 15 | * 16 | * @author thechucklingatom 17 | * 18 | */ 19 | public class Legality implements Serializable{ 20 | private String format; 21 | private String legality; 22 | 23 | public String getFormat() { 24 | return format; 25 | } 26 | 27 | public void setFormat(String format) { 28 | this.format = format; 29 | } 30 | 31 | public String getLegality() { 32 | return legality; 33 | } 34 | 35 | public void setLegality(String legality) { 36 | this.legality = legality; 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o){ 41 | if(o instanceof Legality){ 42 | return ((Legality) o).getFormat().equals(format) 43 | && ((Legality) o).getLegality().equals(legality); 44 | }else{ 45 | return false; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/resource/Ruling.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.resource; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * This file is part of mtgsdk. 7 | * https://github.com/MagicTheGathering/mtg-sdk-java 8 | *

9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT-license 11 | *

12 | * Created by BeMacized on 7/23/2017. 13 | *

14 | * The Ruling sub object in the card json object. 15 | * 16 | * @author BeMacized 17 | */ 18 | public class Ruling implements Serializable { 19 | 20 | private String date; 21 | private String text; 22 | 23 | public String getDate() { 24 | return date; 25 | } 26 | 27 | public void setDate(String date) { 28 | this.date = date; 29 | } 30 | 31 | public String getText() { 32 | return text; 33 | } 34 | 35 | public void setText(String text) { 36 | this.text = text; 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o) { 41 | if (this == o) return true; 42 | if (o == null || getClass() != o.getClass()) return false; 43 | 44 | Ruling ruling = (Ruling) o; 45 | 46 | if (date != null ? !date.equals(ruling.date) : ruling.date != null) return false; 47 | return text != null ? text.equals(ruling.text) : ruling.text == null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /stubby/json/cards/10.json: -------------------------------------------------------------------------------- 1 | { 2 | "card": { 3 | "name": "Crystal Rod", 4 | "manaCost": "{1}", 5 | "cmc": 1, 6 | "type": "Artifact", 7 | "types": [ 8 | "Artifact" 9 | ], 10 | "rarity": "Uncommon", 11 | "set": "LEA", 12 | "setName": "Limited Edition Alpha", 13 | "text": "Whenever a player casts a blue spell, you may pay {1}. If you do, you gain 1 life.", 14 | "artist": "Amy Weber", 15 | "layout": "normal", 16 | "multiverseid": 10, 17 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=10&type=card", 18 | "printings": [ 19 | "LEA", 20 | "LEB", 21 | "2ED", 22 | "CED", 23 | "CEI", 24 | "3ED", 25 | "4ED", 26 | "5ED", 27 | "6ED", 28 | "7ED", 29 | "8ED" 30 | ], 31 | "originalText": "{1}: Any blue spell cast by any player gives you 1 life.", 32 | "originalType": "Poly Artifact", 33 | "legalities": [ 34 | { 35 | "format": "Commander", 36 | "legality": "Legal" 37 | }, 38 | { 39 | "format": "Legacy", 40 | "legality": "Legal" 41 | }, 42 | { 43 | "format": "Modern", 44 | "legality": "Legal" 45 | }, 46 | { 47 | "format": "Vintage", 48 | "legality": "Legal" 49 | } 50 | ], 51 | "id": "5c02996ce42975e3835d47c06b044e974835b511" 52 | } 53 | } -------------------------------------------------------------------------------- /stubby/json/cards/926234c2fe8863f49220a878346c4c5ca79b6046.json: -------------------------------------------------------------------------------- 1 | { 2 | "card": { 3 | "name": "Air Elemental", 4 | "manaCost": "{3}{U}{U}", 5 | "cmc": 5, 6 | "colors": [ 7 | "Blue" 8 | ], 9 | "colorIdentity": [ 10 | "U" 11 | ], 12 | "type": "Creature — Elemental", 13 | "types": [ 14 | "Creature" 15 | ], 16 | "subtypes": [ 17 | "Elemental" 18 | ], 19 | "rarity": "Uncommon", 20 | "set": "LEA", 21 | "setName": "Limited Edition Alpha", 22 | "text": "Flying", 23 | "flavor": "These spirits of the air are winsome and wild, and cannot be truly contained. Only marginally intelligent, they often substitute whimsy for strategy, delighting in mischief and mayhem.", 24 | "artist": "Richard Thomas", 25 | "power": "4", 26 | "toughness": "4", 27 | "layout": "normal", 28 | "multiverseid": 94, 29 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=94&type=card", 30 | "printings": [ 31 | "LEA", 32 | "LEB", 33 | "2ED", 34 | "CED", 35 | "CEI", 36 | "3ED", 37 | "4ED", 38 | "5ED", 39 | "PO2", 40 | "6ED", 41 | "S99", 42 | "BRB", 43 | "BTD", 44 | "7ED", 45 | "8ED", 46 | "9ED", 47 | "10E", 48 | "DD2", 49 | "M10", 50 | "DPA", 51 | "ME4", 52 | "DD3_JVC" 53 | ], 54 | "originalText": "Flying", 55 | "originalType": "Summon — Elemental", 56 | "legalities": [ 57 | { 58 | "format": "Commander", 59 | "legality": "Legal" 60 | }, 61 | { 62 | "format": "Legacy", 63 | "legality": "Legal" 64 | }, 65 | { 66 | "format": "Modern", 67 | "legality": "Legal" 68 | }, 69 | { 70 | "format": "Vintage", 71 | "legality": "Legal" 72 | } 73 | ], 74 | "id": "926234c2fe8863f49220a878346c4c5ca79b6046" 75 | } 76 | } -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/resource/ForeignData.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.resource; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * This file is part of mtgsdk. 7 | * https://github.com/MagicTheGathering/mtg-sdk-java 8 | *

9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT-license 11 | *

12 | * Created by thechucklingatom on 4/07/2019. 13 | *

14 | * Foreign Data class that is created from the JSON set representation. 15 | * 16 | * @author thechucklingatom 17 | */ 18 | public class ForeignData implements Serializable { 19 | private String name; 20 | private String text; 21 | private String flavor; 22 | private String imageUrl; 23 | private String language; 24 | private int multiverseId; 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | 30 | public void setName(String name) { 31 | this.name = name; 32 | } 33 | 34 | public String getText() { 35 | return text; 36 | } 37 | 38 | public void setText(String text) { 39 | this.text = text; 40 | } 41 | 42 | public String getFlavor() { 43 | return flavor; 44 | } 45 | 46 | public void setFlavor(String flavor) { 47 | this.flavor = flavor; 48 | } 49 | 50 | public String getImageUrl() { 51 | return imageUrl; 52 | } 53 | 54 | public void setImageUrl(String imageUrl) { 55 | this.imageUrl = imageUrl; 56 | } 57 | 58 | public String getLanguage() { 59 | return language; 60 | } 61 | 62 | public void setLanguage(String language) { 63 | this.language = language; 64 | } 65 | 66 | public int getMultiverseId() { 67 | return multiverseId; 68 | } 69 | 70 | public void setMultiverseId(int multiverseId) { 71 | this.multiverseId = multiverseId; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /stubby/json/cards/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "card": { 3 | "name": "Ankh of Mishra", 4 | "manaCost": "{2}", 5 | "cmc": 2, 6 | "type": "Artifact", 7 | "types": [ 8 | "Artifact" 9 | ], 10 | "rarity": "Rare", 11 | "set": "LEA", 12 | "setName": "Limited Edition Alpha", 13 | "text": "Whenever a land enters the battlefield, Ankh of Mishra deals 2 damage to that land's controller.", 14 | "artist": "Amy Weber", 15 | "layout": "normal", 16 | "multiverseid": 1, 17 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1&type=card", 18 | "rulings": [ 19 | { 20 | "date": "2004-10-04", 21 | "text": "This triggers on any land entering the battlefield. This includes playing a land or putting a land onto the battlefield using a spell or ability." 22 | }, 23 | { 24 | "date": "2004-10-04", 25 | "text": "It determines the land’s controller at the time the ability resolves. If the land leaves the battlefield before the ability resolves, the land’s last controller before it left is used." 26 | } 27 | ], 28 | "printings": [ 29 | "LEA", 30 | "LEB", 31 | "2ED", 32 | "CED", 33 | "CEI", 34 | "3ED", 35 | "4ED", 36 | "5ED", 37 | "6ED", 38 | "MED", 39 | "VMA" 40 | ], 41 | "originalText": "Ankh does 2 damage to anyone who puts a new land into play.", 42 | "originalType": "Continuous Artifact", 43 | "legalities": [ 44 | { 45 | "format": "Commander", 46 | "legality": "Legal" 47 | }, 48 | { 49 | "format": "Legacy", 50 | "legality": "Legal" 51 | }, 52 | { 53 | "format": "Vintage", 54 | "legality": "Legal" 55 | } 56 | ], 57 | "id": "8a5d85644f546525433c4472b76c3b0ebb495b33" 58 | } 59 | } -------------------------------------------------------------------------------- /stubby/json/cards/8a5d85644f546525433c4472b76c3b0ebb495b33.json: -------------------------------------------------------------------------------- 1 | { 2 | "card": { 3 | "name": "Ankh of Mishra", 4 | "manaCost": "{2}", 5 | "cmc": 2, 6 | "type": "Artifact", 7 | "types": [ 8 | "Artifact" 9 | ], 10 | "rarity": "Rare", 11 | "set": "LEA", 12 | "setName": "Limited Edition Alpha", 13 | "text": "Whenever a land enters the battlefield, Ankh of Mishra deals 2 damage to that land's controller.", 14 | "artist": "Amy Weber", 15 | "layout": "normal", 16 | "multiverseid": 1, 17 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1&type=card", 18 | "rulings": [ 19 | { 20 | "date": "2004-10-04", 21 | "text": "This triggers on any land entering the battlefield. This includes playing a land or putting a land onto the battlefield using a spell or ability." 22 | }, 23 | { 24 | "date": "2004-10-04", 25 | "text": "It determines the land’s controller at the time the ability resolves. If the land leaves the battlefield before the ability resolves, the land’s last controller before it left is used." 26 | } 27 | ], 28 | "printings": [ 29 | "LEA", 30 | "LEB", 31 | "2ED", 32 | "CED", 33 | "CEI", 34 | "3ED", 35 | "4ED", 36 | "5ED", 37 | "6ED", 38 | "MED", 39 | "VMA" 40 | ], 41 | "originalText": "Ankh does 2 damage to anyone who puts a new land into play.", 42 | "originalType": "Continuous Artifact", 43 | "legalities": [ 44 | { 45 | "format": "Commander", 46 | "legality": "Legal" 47 | }, 48 | { 49 | "format": "Legacy", 50 | "legality": "Legal" 51 | }, 52 | { 53 | "format": "Vintage", 54 | "legality": "Legal" 55 | } 56 | ], 57 | "id": "8a5d85644f546525433c4472b76c3b0ebb495b33" 58 | } 59 | } -------------------------------------------------------------------------------- /src/test/java/io/magicthegathering/javasdk/api/SetAPITest.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.api; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import io.magicthegathering.javasdk.resource.Card; 9 | import io.magicthegathering.javasdk.resource.MtgSet; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | import static org.junit.Assert.assertFalse; 13 | import static org.junit.Assert.assertNotEquals; 14 | import static org.junit.Assert.assertNotNull; 15 | import static org.junit.Assert.assertNull; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | public class SetAPITest extends MTGAPITest { 19 | @Test 20 | public void testGetSet() { 21 | MtgSet testSet = new MtgSet(); 22 | testSet.setGatherercode("1E"); 23 | assertEquals(testSet, SetAPI.getSet("LEA")); 24 | assertNotEquals(testSet, SetAPI.getSet("LEB")); 25 | } 26 | 27 | @Test 28 | public void testBadSetId() { 29 | assertNull(SetAPI.getSet("666")); 30 | } 31 | 32 | @Test 33 | public void testGetAllSets() { 34 | List testSetList = SetAPI.getAllSets(); 35 | MtgSet testSet = new MtgSet(); 36 | testSet.setGatherercode("1E"); 37 | assertEquals(testSetList.get(0), testSet); 38 | } 39 | 40 | @Test 41 | public void testGetBoosterFromSet() { 42 | String setCode = "KLD"; 43 | List booster = SetAPI.getBooster(setCode); 44 | assertEquals(15, booster.size()); 45 | } 46 | 47 | @Test 48 | public void testSetFilter(){ 49 | ArrayList filter = new ArrayList<>(); 50 | filter.add("name=Alpha"); 51 | 52 | MtgSet alpha = SetAPI.getSet("LEA"); 53 | 54 | assertTrue(SetAPI.getAllSets(filter).contains(alpha)); 55 | } 56 | 57 | @Test 58 | public void testSetGetCards() { 59 | MtgSet testSet; 60 | testSet = SetAPI.getSet("LEA"); 61 | 62 | assertNotNull(testSet.getCards()); 63 | 64 | Card testCard = new Card(); 65 | testCard.setMultiverseid(94); 66 | testCard.setName("Air Elemental"); 67 | testCard.setCmc(5); 68 | 69 | assertTrue(testSet.getCards().contains(testCard)); 70 | } 71 | 72 | @Test 73 | public void testGetAllSetsWithCards() { 74 | List sets = SetAPI.getAllSetsWithCards(); 75 | 76 | assertNotNull(sets.get(0).getCards()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/api/CardAPI.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.api; 2 | 3 | import io.magicthegathering.javasdk.resource.Card; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * {@link CardAPI} is used to fetch {@link Card}s from magicthegathering.io 9 | * 10 | * @author nniklas 11 | */ 12 | public class CardAPI extends MTGAPI { 13 | private static final String RESOURCE_PATH = "cards"; 14 | 15 | /** 16 | * @return A {@link Card} based on the given cardid 17 | */ 18 | public static Card getCard(String cardId) { 19 | String path = String.format("%s/%s/", RESOURCE_PATH, cardId); 20 | return get(path, "card", Card.class); 21 | } 22 | 23 | /** 24 | * @return A {@link Card} based on the given multiverseid 25 | */ 26 | public static Card getCard(int multiverseId) { 27 | String path = String.format("%s/%s/", RESOURCE_PATH, multiverseId); 28 | return get(path, "card", Card.class); 29 | } 30 | 31 | /** 32 | * @return All the available {@link Card}s as a list. 33 | */ 34 | public static List getAllCards() { 35 | return getList(RESOURCE_PATH, "cards", Card.class); 36 | } 37 | 38 | /** 39 | * @return A {@link List} of all card types as {@link String}s. 40 | * 41 | * @see 42 | * https://docs.magicthegathering.io/#card-types 43 | */ 44 | public static List getAllCardTypes() { 45 | String path = "types"; 46 | return getList(path, "types", String.class); 47 | } 48 | 49 | /** 50 | * @return A {@link List} of all card types as {@link String}s. 51 | * 52 | * @see 53 | * https://docs.magicthegathering.io/#get-all-supertypes 54 | */ 55 | public static List getAllCardSupertypes() { 56 | String path = "supertypes"; 57 | return getList(path, "supertypes", String.class); 58 | } 59 | 60 | /** 61 | * @return A {@link List} of all card types as {@link String}s. 62 | * 63 | * @see 64 | * https://docs.magicthegathering.io/#card-types 65 | */ 66 | public static List getAllCardSubtypes() { 67 | String path = "subtypes"; 68 | return getList(path, "subtypes", String.class); 69 | } 70 | 71 | /** 72 | * Get all the {@link Card} that match a certain filter. 73 | * @param filters List of filters supported by the web API 74 | * @return List of all matching {@link Card}s. 75 | * 76 | * @see 77 | * https://docs.magicthegathering.io/#cards 78 | */ 79 | public static List getAllCards(List filters){ 80 | return getList(RESOURCE_PATH, "cards", Card.class, filters); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/api/SetAPI.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.api; 2 | 3 | import java.util.Collections; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | 7 | import io.magicthegathering.javasdk.resource.Card; 8 | import io.magicthegathering.javasdk.resource.MtgSet; 9 | 10 | /** 11 | * {@link SetAPI} is used to fetch {@link MtgSet}s from magicthegathering.io 12 | * 13 | * @author nniklas 14 | */ 15 | public class SetAPI extends MTGAPI { 16 | private static final String RESOURCE_PATH = "sets"; 17 | 18 | /** 19 | * Returns a {@link MtgSet} based on the given set code. 20 | * @param setCode Code to find the specific set. 21 | */ 22 | public static MtgSet getSet(String setCode) { 23 | String path = String.format("%s/%s/", RESOURCE_PATH, setCode); 24 | MtgSet returnSet = get(path, "set", MtgSet.class); 25 | if(returnSet != null) { 26 | returnSet.setCards(CardAPI.getAllCards(new LinkedList<>(Collections.singletonList("set=" + setCode)))); 27 | } 28 | return returnSet; 29 | } 30 | 31 | /** 32 | * The method that returns all the {@link MtgSet}. 33 | * If you want all the card lists populated use 34 | * {@link #getAllSetsWithCards()}. 35 | * @return A List of all the sets. 36 | */ 37 | public static List getAllSets() { 38 | return getList(RESOURCE_PATH, "sets", MtgSet.class); 39 | } 40 | 41 | /** 42 | * The method that will generate a booster for the selected {@link MtgSet} 43 | * @param setCode Code of which set you want a booster for. 44 | * @return the randomized booster for the set. 45 | */ 46 | public static List getBooster(String setCode) { 47 | String path = String.format("%s/%s/%s/", RESOURCE_PATH, setCode, 48 | "booster"); 49 | return getList(path, "cards", Card.class); 50 | } 51 | 52 | /** 53 | * Gets a list of {@link MtgSet} based on the provided filters in the 54 | * web API documentation. 55 | * @param filters List of string filters 56 | * @return The list of {@link MtgSet}s that was found by the filter. 57 | */ 58 | public static List getAllSets(List filters){ 59 | return getList(RESOURCE_PATH, "sets", MtgSet.class, filters); 60 | } 61 | 62 | /** 63 | * Gets a list of {@link MtgSet} with all the card objects populated. 64 | * Performance will be degraded because of all the Api calls that will 65 | * happen. 66 | * @return A list of all the sets with cards populated. 67 | */ 68 | public static List getAllSetsWithCards() { 69 | List returnList = getList(RESOURCE_PATH, "sets", MtgSet.class); 70 | for(MtgSet set : returnList){ 71 | set.setCards(CardAPI.getAllCards(new LinkedList<>(Collections.singletonList("set=" + set.getCode())))); 72 | } 73 | return returnList; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Magic: The Gathering Java SDK 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/MagicTheGathering/mtg-sdk-java.svg?branch=master)](https://travis-ci.org/MagicTheGathering/mtg-sdk-java) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.magicthegathering/javasdk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.magicthegathering/javasdk) 6 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/9bb4a9c574ad44138d41168ff7095633)](https://www.codacy.com/app/nyholmniklas/mtg-sdk-java?utm_source=github.com&utm_medium=referral&utm_content=MagicTheGathering/mtg-sdk-java&utm_campaign=Badge_Grade) 7 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/MagicTheGathering/mtg-sdk-java/blob/master/LICENSE) 8 | [![mtg-developers on discord](https://img.shields.io/badge/discord-mtg%20developers-738bd7.svg)](https://discord.gg/qwGJNnP) 9 | 10 | Java SDK for using the [magicthegathering.io](http://magicthegathering.io) APIs. 11 | 12 | Note that API use is free and does not require authentication or registration, but some rate limits apply. Read the official API website for more information. 13 | 14 | Add the dependency to your project and you're good to go! If you are on Android make sure you call on a seperate thread than the main. 15 | 16 | Prerequisites 17 | ------- 18 | - Java JDK 7 or higher 19 | 20 | Integration 21 | ------- 22 | 23 | #### Maven 24 | ```xml 25 | 26 | io.magicthegathering 27 | javasdk 28 | 0.0.18 29 | 30 | ``` 31 | #### Gradle 32 | ```gradle 33 | implementation 'io.magicthegathering:javasdk:0.0.18' 34 | ``` 35 | 36 | #### Ivy 37 | ```xml 38 | 39 | ``` 40 | 41 | Usage examples 42 | ------- 43 | 44 | #### Get a Card 45 | ```java 46 | int multiverseId = 1; 47 | Card card = CardAPI.getCard(multiverseId); 48 | ``` 49 | 50 | #### Get all Cards 51 | ```java 52 | List cards = CardAPI.getAllCards(); 53 | ``` 54 | 55 | #### Get a Set 56 | ```java 57 | String setCode = "KLD"; 58 | MtgSet set = SetAPI.getSet(setCode); 59 | ``` 60 | 61 | #### Get all Sets 62 | This does **not** populate the card lists by default. This is to improve perfomance if all you need is a set list. 63 | Filter also does not currently load set lists. Will be adding in a future release. 64 | ```java 65 | List sets = SetAPI.getAllSets(); 66 | ``` 67 | 68 | #### Get all Sets with card lists loaded. 69 | ```java 70 | List sets = SetAPI.getAllSetsWithCards(); 71 | ``` 72 | 73 | #### Generate a Booster 74 | ```java 75 | String setCode = "KLD"; 76 | List booster = SetAPI.getBooster(setCode); 77 | ``` 78 | #### Change the connection timeout values that are used by the OkHttpClient 79 | ```java 80 | MTGAPI.setConnectTimeout(60); 81 | MTGAPI.setReadTimeout(60); 82 | MTGAPI.setWriteTimeout(60); 83 | ``` 84 | License 85 | ------- 86 | This project is licensed under [MIT license](http://opensource.org/licenses/MIT). 87 | -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/resource/MtgSet.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.resource; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * This file is part of mtgsdk. 7 | * https://github.com/MagicTheGathering/mtg-sdk-java 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT-license 11 | * 12 | * Created by thechucklingatom on 2/16/2016. 13 | * 14 | * Set class that is created from the JSON set representation. 15 | * 16 | * @author thechucklingatom 17 | */ 18 | public class MtgSet { 19 | private String name; 20 | private String code; 21 | private String gathererCode; 22 | private String oldCode; 23 | private String magicCardsInfoCode; 24 | private String releaseDate; 25 | private String border; 26 | private String type; 27 | private String block; 28 | private boolean onlineOnly; 29 | private List booster; 30 | private List cards; 31 | 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | public void setName(String name) { 37 | this.name = name; 38 | } 39 | 40 | public String getCode() { 41 | return code; 42 | } 43 | 44 | public void setCode(String code) { 45 | this.code = code; 46 | } 47 | 48 | public String getGatherercode() { 49 | return gathererCode; 50 | } 51 | 52 | public void setGatherercode(String gatherercode) { 53 | this.gathererCode = gatherercode; 54 | } 55 | 56 | public String getOldCode() { 57 | return oldCode; 58 | } 59 | 60 | public void setOldCode(String oldCode) { 61 | this.oldCode = oldCode; 62 | } 63 | 64 | public String getMagicCardsInfoCode() { 65 | return magicCardsInfoCode; 66 | } 67 | 68 | public void setMagicCardsInfoCode(String magicCardsInfoCode) { 69 | this.magicCardsInfoCode = magicCardsInfoCode; 70 | } 71 | 72 | public String getReleaseDate() { 73 | return releaseDate; 74 | } 75 | 76 | public void setReleaseDate(String releaseDate) { 77 | this.releaseDate = releaseDate; 78 | } 79 | 80 | public String getBorder() { 81 | return border; 82 | } 83 | 84 | public void setBorder(String border) { 85 | this.border = border; 86 | } 87 | 88 | public String getType() { 89 | return type; 90 | } 91 | 92 | public void setType(String type) { 93 | this.type = type; 94 | } 95 | 96 | public String getBlock() { 97 | return block; 98 | } 99 | 100 | public void setBlock(String block) { 101 | this.block = block; 102 | } 103 | 104 | public boolean getOnlineOnly() { 105 | return onlineOnly; 106 | } 107 | 108 | public void setOnlineOnly(boolean onlineOnly) { 109 | this.onlineOnly = onlineOnly; 110 | } 111 | 112 | public List getBooster() { 113 | return booster; 114 | } 115 | 116 | public void setBooster(List booster) { 117 | this.booster = booster; 118 | } 119 | 120 | public List getCards() { 121 | return cards; 122 | } 123 | 124 | public void setCards(List cards) { 125 | this.cards = cards; 126 | } 127 | 128 | /** 129 | * dirty compare to in order to start testing. Just comparing the setGathererCode 130 | * which should be unique. May change to just the code. 131 | * @param toCompare A {@link MtgSet} object hopefully 132 | * @return true if the same set, false if different. 133 | */ 134 | @Override 135 | public boolean equals(Object toCompare){ 136 | if(toCompare instanceof MtgSet){ 137 | MtgSet cardCompare = (MtgSet)toCompare; 138 | return getGatherercode().equalsIgnoreCase(cardCompare.getGatherercode()); 139 | }else{ 140 | return false; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/test/java/io/magicthegathering/javasdk/api/CardAPITest.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.api; 2 | 3 | import io.magicthegathering.javasdk.resource.Card; 4 | import io.magicthegathering.javasdk.resource.Legality; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import org.junit.Test; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | public class CardAPITest extends MTGAPITest { 15 | 16 | @Test 17 | public void testGetCardByMultiverseId() { 18 | Card testCard = new Card(); 19 | testCard.setMultiverseid(1); 20 | testCard.setName("Ankh of Mishra"); 21 | testCard.setCmc(2); 22 | assertEquals(testCard, CardAPI.getCard(1)); 23 | assertFalse(testCard.equals(CardAPI.getCard(10))); 24 | } 25 | 26 | @Test 27 | public void testGetCardById() { 28 | Card testCard = new Card(); 29 | testCard.setMultiverseid(1); 30 | testCard.setName("Ankh of Mishra"); 31 | testCard.setCmc(2); 32 | assertEquals(testCard, CardAPI.getCard("8a5d85644f546525433c4472b76c3b0ebb495b33")); 33 | assertNotEquals(testCard, CardAPI.getCard("926234c2fe8863f49220a878346c4c5ca79b6046")); 34 | } 35 | 36 | @Test 37 | public void testBadCardId() throws Exception { 38 | assertNull(CardAPI.getCard(-1)); 39 | } 40 | 41 | @Test 42 | public void testGetAll() throws Exception { 43 | List testCardList = CardAPI.getAllCards(); 44 | Card testCard = new Card(); 45 | testCard.setMultiverseid(94); 46 | testCard.setName("Air Elemental"); 47 | testCard.setCmc(5); 48 | assertEquals(testCardList.get(0), testCard); 49 | } 50 | 51 | @Test 52 | public void testDeserializePictureUrl() throws Exception { 53 | List testCardList = CardAPI.getAllCards(); 54 | assertEquals("http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=94&type=card", testCardList.get(0).getImageUrl()); 55 | } 56 | 57 | @Test 58 | public void testGetAllCardTypes() throws Exception { 59 | List types = CardAPI.getAllCardTypes(); 60 | assertTrue(types.contains("Artifact")); 61 | assertTrue(types.contains("Creature")); 62 | assertTrue(types.contains("Planeswalker")); 63 | } 64 | 65 | @Test 66 | public void testGetAllCardSupertypes() throws Exception { 67 | List superTypes = CardAPI.getAllCardSupertypes(); 68 | assertTrue(superTypes.contains("Legendary")); 69 | assertTrue(superTypes.contains("Basic")); 70 | assertTrue(superTypes.contains("Snow")); 71 | } 72 | 73 | @Test 74 | public void testGetAllCardSubtypes() throws Exception { 75 | List superTypes = CardAPI.getAllCardSubtypes(); 76 | assertTrue(superTypes.contains("Ape")); 77 | assertTrue(superTypes.contains("Elf")); 78 | assertTrue(superTypes.contains("Squid")); 79 | } 80 | 81 | @Test 82 | public void testCardFilter(){ 83 | ArrayList filter = new ArrayList<>(); 84 | filter.add("name=Air"); 85 | 86 | Card testCard = new Card(); 87 | testCard.setMultiverseid(94); 88 | testCard.setName("Air Elemental"); 89 | testCard.setCmc(5); 90 | assertTrue(CardAPI.getAllCards(filter).contains(testCard)); 91 | } 92 | 93 | @Test 94 | public void testLegality() { 95 | Legality testLegality = new Legality(); 96 | testLegality.setFormat("Commander"); 97 | testLegality.setLegality("Legal"); 98 | assertEquals(testLegality, CardAPI.getCard(1).getLegalities()[0]); 99 | } 100 | 101 | @Test 102 | public void testEquality() { 103 | Card testCard = new Card(); 104 | testCard.setName("Gifts Given"); 105 | testCard.setCmc(4); 106 | assertEquals(testCard, CardAPI.getAllCards(Collections.singletonList("name=Gifts Given")).get(0)); 107 | 108 | testCard.setName("Nicol Bolas, the Arisen"); 109 | testCard.setManaCost(null); 110 | testCard.setMultiverseid(447355); 111 | assertEquals(testCard, CardAPI.getAllCards(Collections.singletonList("name=Nicol Bolas, the Arisen")).get(0)); 112 | } 113 | 114 | @Test 115 | public void testVariations() { 116 | assertNotNull(CardAPI.getCard("ffa00e95-754e-5484-8e4c-e3b707d4c1d2").getVariations()); 117 | } 118 | 119 | @Test 120 | public void testForeignData() { 121 | Card testCard = CardAPI.getAllCards(Collections.singletonList("name=Nicol Bolas, the Arisen")).get(0); 122 | assertNotNull(testCard.getForeignNames()); 123 | assertEquals(testCard.getForeignNames()[0].getName(), "Nicol Bolas, der Emporgestiegene"); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | io.magicthegathering 6 | javasdk 7 | 0.0.18 8 | jar 9 | 10 | ${project.groupId}:${project.artifactId} 11 | 12 | A Java SDK for accessing the magicthegathering.io HTTP API 13 | 14 | https://github.com/MagicTheGathering/mtg-sdk-java 15 | 16 | 17 | 18 | MIT License 19 | http://www.opensource.org/licenses/mit-license.php 20 | 21 | 22 | 23 | 24 | 25 | Niklas Nyholm 26 | nyholmniklas@gmail.com 27 | magicthegathering.io 28 | http://www.magicthegathering.io 29 | 30 | 31 | Robert Putnam 32 | thechucklingatom@gmail.com 33 | magicthegathering.io 34 | http://www.magicthegathering.io 35 | 36 | 37 | 38 | 39 | scm:git:git://github.com/MagicTheGathering/mtg-sdk-java.git 40 | scm:git:ssh://github.com/MagicTheGathering/mtg-sdk-java.git 41 | https://github.com/MagicTheGathering/mtg-sdk-java 42 | 43 | 44 | 45 | UTF-8 46 | 47 | 48 | 49 | 50 | ossrh 51 | https://oss.sonatype.org/content/repositories/snapshots 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.apache.maven.plugins 59 | maven-compiler-plugin 60 | 3.1 61 | 62 | 1.7 63 | 1.7 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-source-plugin 69 | 2.2.1 70 | 71 | 72 | attach-sources 73 | 74 | jar-no-fork 75 | 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-javadoc-plugin 82 | 2.9.1 83 | 84 | 85 | attach-javadocs 86 | 87 | jar 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-gpg-plugin 95 | 1.5 96 | 97 | 98 | sign-artifacts 99 | verify 100 | 101 | sign 102 | 103 | 104 | 105 | 106 | 107 | org.sonatype.plugins 108 | nexus-staging-maven-plugin 109 | 1.6.7 110 | true 111 | 112 | ossrh 113 | https://oss.sonatype.org/ 114 | true 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-site-plugin 120 | 3.7.1 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-project-info-reports-plugin 125 | 3.0.0 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | com.google.code.gson 134 | gson 135 | 2.8.9 136 | 137 | 138 | 139 | 140 | com.squareup.okhttp3 141 | okhttp 142 | 4.12.0 143 | 144 | 145 | 146 | junit 147 | junit 148 | 4.13.1 149 | test 150 | 151 | 152 | 153 | by.stub 154 | stubby4j 155 | 3.3.0 156 | test 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /stubby/json/cards/ffa00e95-754e-5484-8e4c-e3b707d4c1d2.json: -------------------------------------------------------------------------------- 1 | { 2 | "card": { 3 | "name": "Angel of Mercy", 4 | "manaCost": "{4}{W}", 5 | "cmc": 5.0, 6 | "colors": [ 7 | "White" 8 | ], 9 | "colorIdentity": [ 10 | "W" 11 | ], 12 | "type": "Creature — Angel", 13 | "supertypes": [], 14 | "types": [ 15 | "Creature" 16 | ], 17 | "subtypes": [ 18 | "Angel" 19 | ], 20 | "rarity": "Uncommon", 21 | "set": "10E", 22 | "setName": "Tenth Edition", 23 | "text": "Flying\nWhen Angel of Mercy enters the battlefield, you gain 3 life.", 24 | "flavor": "Every tear shed is a drop of immortality.", 25 | "artist": "Volkan Baga", 26 | "number": "2★", 27 | "power": "3", 28 | "toughness": "3", 29 | "layout": "normal", 30 | "variations": [ 31 | "2d040ab5-3430-5505-8fd0-a99666a3913e" 32 | ], 33 | "rulings": [], 34 | "foreignNames": [ 35 | { 36 | "name": "Engel der Gnade", 37 | "text": "Fliegend (Diese Kreatur kann außer von fliegenden Kreaturen und Kreaturen mit Reichweite nicht geblockt werden.)\nWenn der Engel der Gnade ins Spiel kommt, erhältst du 3 Lebenspunkte dazu.", 38 | "flavor": "Jede ihrer Tränen ist ein Tropfen Unsterblichkeit.", 39 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=148412&type=card", 40 | "language": "German", 41 | "multiverseid": 148412 42 | }, 43 | { 44 | "name": "Ángel de piedad", 45 | "text": "Vuela. (Esta criatura no puede ser bloqueada excepto por criaturas que tengan la habilidad de volar o alcance.)\nCuando el Ángel de piedad entre en juego, gana 3 vidas.", 46 | "flavor": "Cada lágrima derramada es una gota de inmortalidad.", 47 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=150318&type=card", 48 | "language": "Spanish", 49 | "multiverseid": 150318 50 | }, 51 | { 52 | "name": "Ange de miséricorde", 53 | "text": "Vol (Cette créature ne peut être bloquée que par des créatures avec le vol ou la portée.)\nQuand l'Ange de miséricorde arrive en jeu, vous gagnez 3 points de vie.", 54 | "flavor": "Chaque larme versée est une goutte d'immortalité.", 55 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=149935&type=card", 56 | "language": "French", 57 | "multiverseid": 149935 58 | }, 59 | { 60 | "name": "Angelo della Misericordia", 61 | "text": "Volare (Questa creatura non può essere bloccata tranne che da creature con volare o raggiungere.)\nQuando l'Angelo della Misericordia entra in gioco, guadagni 3 punti vita.", 62 | "flavor": "Ogni lacrima versata è una goccia d'immortalità.", 63 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=148795&type=card", 64 | "language": "Italian", 65 | "multiverseid": 148795 66 | }, 67 | { 68 | "name": "慈悲の天使", 69 | "text": "飛行 (このクリーチャーは飛行や到達を持たないクリーチャーによってブロックされない。)\n慈悲の天使が場に出たとき、あなたは3点のライフを得る。", 70 | "flavor": "流す涙は不死の滴。", 71 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=148029&type=card", 72 | "language": "Japanese", 73 | "multiverseid": 148029 74 | }, 75 | { 76 | "name": "Anjo de Misericórdia", 77 | "text": "Voar (Esta criatura só pode ser bloqueada por criaturas com a habilidade de voar ou alcance.)\nQuando Anjo de Misericórdia entra em jogo, você ganha 3 pontos de vida.", 78 | "flavor": "Cada lágrima derramada é uma gota de imortalidade.", 79 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=149552&type=card", 80 | "language": "Portuguese (Brazil)", 81 | "multiverseid": 149552 82 | }, 83 | { 84 | "name": "Ангел Милосердия", 85 | "text": "Полет (Это существо может быть заблокировано только существом с Полетом или Захватом.)\nКогда Ангел Милосердия входит в игру, вы получаете 3 жизни.", 86 | "flavor": "Каждая пролитая слеза это капля бессмертия.", 87 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=149169&type=card", 88 | "language": "Russian", 89 | "multiverseid": 149169 90 | }, 91 | { 92 | "name": "慈悲天使", 93 | "text": "飞行(只有具飞行或延势异能的生物才能阻挡它。)\n当慈悲天使进场时,你获得3点生命。", 94 | "flavor": "每颗泪珠都是一滴不朽。", 95 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=147646&type=card", 96 | "language": "Chinese Simplified", 97 | "multiverseid": 147646 98 | } 99 | ], 100 | "printings": [ 101 | "10E", 102 | "8ED", 103 | "9ED", 104 | "DDC", 105 | "DVD", 106 | "IMA", 107 | "INV", 108 | "P02", 109 | "PS11", 110 | "PSAL", 111 | "S99" 112 | ], 113 | "legalities": [ 114 | { 115 | "format": "1v1", 116 | "legality": "Legal" 117 | }, 118 | { 119 | "format": "Commander", 120 | "legality": "Legal" 121 | }, 122 | { 123 | "format": "Duel", 124 | "legality": "Legal" 125 | }, 126 | { 127 | "format": "Legacy", 128 | "legality": "Legal" 129 | }, 130 | { 131 | "format": "Modern", 132 | "legality": "Legal" 133 | }, 134 | { 135 | "format": "Pauper", 136 | "legality": "Legal" 137 | }, 138 | { 139 | "format": "Penny", 140 | "legality": "Legal" 141 | }, 142 | { 143 | "format": "Vintage", 144 | "legality": "Legal" 145 | } 146 | ], 147 | "id": "ffa00e95-754e-5484-8e4c-e3b707d4c1d2" 148 | } 149 | } -------------------------------------------------------------------------------- /stubby/stubby-config.yaml: -------------------------------------------------------------------------------- 1 | - request: 2 | url: ^/cards 3 | method: GET 4 | query: 5 | name: "Gifts%20Given" 6 | response: 7 | status: 200 8 | headers: 9 | Content-Type: application/json 10 | file: json/cards/GiftsGiven.json 11 | 12 | - request: 13 | url: ^/cards 14 | method: GET 15 | query: 16 | name: "Gifts%20Given" 17 | page: 1 18 | response: 19 | status: 200 20 | headers: 21 | Content-Type: application/json 22 | file: json/cards/GiftsGiven.json 23 | 24 | - request: 25 | url: ^/cards 26 | method: GET 27 | query: 28 | name: "Gifts%20Given" 29 | page: 2 30 | response: 31 | status: 200 32 | headers: 33 | Content-Type: application/json 34 | file: json/cards/GiftsGiven.json 35 | 36 | - request: 37 | url: ^/cards 38 | method: GET 39 | query: 40 | name: "Nicol%20Bolas,%20the%20Arisen" 41 | response: 42 | status: 200 43 | headers: 44 | Content-Type: application/json 45 | file: json/cards/NicolBolasTheArisen.json 46 | 47 | - request: 48 | url: ^/cards 49 | method: GET 50 | query: 51 | name: "Nicol%20Bolas,%20the%20Arisen" 52 | page: 1 53 | response: 54 | status: 200 55 | headers: 56 | Content-Type: application/json 57 | file: json/cards/NicolBolasTheArisen.json 58 | 59 | - request: 60 | url: ^/cards 61 | method: GET 62 | query: 63 | name: "Nicol%20Bolas,%20the%20Arisen" 64 | page: 2 65 | response: 66 | status: 200 67 | headers: 68 | Content-Type: application/json 69 | file: json/cards/NicolBolasTheArisen.json 70 | 71 | - request: 72 | url: ^/cards/?$ 73 | method: GET 74 | response: 75 | status: 200 76 | headers: 77 | Content-Type: application/json 78 | Link: ; rel="last", ; rel="next" 79 | file: json/cards/all.json 80 | 81 | - request: 82 | url: ^/cards/?&page=2 83 | method: GET 84 | response: 85 | status: 200 86 | headers: 87 | Content-Type: application/json 88 | file: json/cards/allPage2.json 89 | 90 | - request: 91 | url: ^/cards/1/?$ 92 | method: GET 93 | response: 94 | status: 200 95 | headers: 96 | Content-Type: application/json 97 | file: json/cards/1.json 98 | 99 | - request: 100 | url: ^/cards/10/?$ 101 | method: GET 102 | response: 103 | status: 200 104 | headers: 105 | Content-Type: application/json 106 | file: json/cards/10.json 107 | 108 | - request: 109 | url: ^/cards/8a5d85644f546525433c4472b76c3b0ebb495b33/?$ 110 | method: GET 111 | response: 112 | status: 200 113 | headers: 114 | Content-Type: application/json 115 | file: json/cards/8a5d85644f546525433c4472b76c3b0ebb495b33.json 116 | 117 | - request: 118 | url: ^/cards/926234c2fe8863f49220a878346c4c5ca79b6046/?$ 119 | method: GET 120 | response: 121 | status: 200 122 | headers: 123 | Content-Type: application/json 124 | file: json/cards/926234c2fe8863f49220a878346c4c5ca79b6046.json 125 | 126 | - request: 127 | url: ^/cards/ffa00e95-754e-5484-8e4c-e3b707d4c1d2/?$ 128 | method: GET 129 | response: 130 | status: 200 131 | headers: 132 | Content-Type: application/json 133 | file: json/cards/ffa00e95-754e-5484-8e4c-e3b707d4c1d2.json 134 | 135 | - request: 136 | url: ^/cards?set=DRK$ 137 | method: GET 138 | response: 139 | status: 200 140 | headers: 141 | Content-Type: application/json 142 | file: json/cards/DRKpage1.json 143 | 144 | - request: 145 | url: ^/cards?set=DRK&page=1$ 146 | method: GET 147 | response: 148 | status: 200 149 | headers: 150 | Content-Type: application/json 151 | file: json/cards/DRKpage1.json 152 | 153 | - request: 154 | url: ^/cards?set=DRK&page=2$ 155 | method: GET 156 | response: 157 | status: 200 158 | headers: 159 | Content-Type: application/json 160 | file: json/cards/DRKpage2.json 161 | 162 | - request: 163 | url: ^/cards/-1/?$ 164 | method: GET 165 | response: 166 | status: 404 167 | headers: 168 | Content-Type: application/json 169 | body: > 170 | {"status":"404","error":"Not Found"} 171 | 172 | - request: 173 | url: ^/types/?$ 174 | method: GET 175 | response: 176 | status: 200 177 | headers: 178 | Content-Type: application/json 179 | file: json/types/types.json 180 | 181 | - request: 182 | url: ^/supertypes/?$ 183 | method: GET 184 | response: 185 | status: 200 186 | headers: 187 | Content-Type: application/json 188 | file: json/types/supertypes.json 189 | 190 | - request: 191 | url: ^/subtypes/?$ 192 | method: GET 193 | response: 194 | status: 200 195 | headers: 196 | Content-Type: application/json 197 | file: json/types/subtypes.json 198 | 199 | - request: 200 | url: ^/sets/?$ 201 | method: GET 202 | response: 203 | status: 200 204 | headers: 205 | Content-Type: application/json 206 | file: json/sets/all.json 207 | 208 | - request: 209 | url: ^/sets/LEA/?$ 210 | method: GET 211 | response: 212 | status: 200 213 | headers: 214 | Content-Type: application/json 215 | file: json/sets/LEA.json 216 | 217 | - request: 218 | url: ^/sets/LEB/?$ 219 | method: GET 220 | response: 221 | status: 200 222 | headers: 223 | Content-Type: application/json 224 | file: json/sets/LEB.json 225 | 226 | - request: 227 | url: ^/sets/DRK/?$ 228 | method: GET 229 | response: 230 | status: 200 231 | headers: 232 | Content-Type: application/json 233 | file: json/sets/DRK.json 234 | 235 | - request: 236 | url: ^/sets/666/?$ 237 | method: GET 238 | response: 239 | status: 404 240 | headers: 241 | Content-Type: application/json 242 | body: > 243 | {"status":"404","error":"Not Found"} 244 | 245 | - request: 246 | url: ^/sets/KLD/booster/?$ 247 | method: GET 248 | response: 249 | status: 200 250 | headers: 251 | Content-Type: application/json 252 | file: json/sets/KLD-booster.json 253 | 254 | -------------------------------------------------------------------------------- /stubby/json/cards/NicolBolasTheArisen.json: -------------------------------------------------------------------------------- 1 | { 2 | "cards": [ 3 | { 4 | "name": "Nicol Bolas, the Arisen", 5 | "names": [ 6 | "Nicol Bolas, the Ravager", 7 | "Nicol Bolas, the Arisen" 8 | ], 9 | "cmc": 4, 10 | "colors": [ 11 | "Blue", 12 | "Black", 13 | "Red" 14 | ], 15 | "colorIdentity": [ 16 | "U", 17 | "B", 18 | "R" 19 | ], 20 | "type": "Legendary Planeswalker — Bolas", 21 | "supertypes": [ 22 | "Legendary" 23 | ], 24 | "types": [ 25 | "Planeswalker" 26 | ], 27 | "subtypes": [ 28 | "Bolas" 29 | ], 30 | "rarity": "Mythic Rare", 31 | "set": "M19", 32 | "setName": "Core Set 2019", 33 | "text": "+2: Draw two cards.\n−3: Nicol Bolas, the Arisen deals 10 damage to target creature or planeswalker.\n−4: Put target creature or planeswalker card from a graveyard onto the battlefield under your control.\n−12: Exile all but the bottom card of target player's library.", 34 | "artist": "Svetlin Velinov", 35 | "number": "218b", 36 | "layout": "double-faced", 37 | "multiverseid": 447355, 38 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=447355&type=card", 39 | "loyalty": 7, 40 | "rulings": [ 41 | { 42 | "date": "2018-07-13", 43 | "text": "In a multiplayer game, if a player leaves the game, all cards that player owns leave as well. If you leave the game, any permanents you control from Nicol Bolas's third loyalty ability that came from other players' graveyards are exiled." 44 | }, 45 | { 46 | "date": "2018-07-13", 47 | "text": "The converted mana cost of a double-faced card is the converted mana cost of its front face, even while it's on the battlefield with its back face up. For example, the converted mana cost of Nicol Bolas, the Arisen is 4." 48 | }, 49 | { 50 | "date": "2018-07-13", 51 | "text": "Nicol Bolas, the Arisen has a color indicator on its typeline. This color indicator means that it's a blue, black, and red permanent." 52 | }, 53 | { 54 | "date": "2018-07-13", 55 | "text": "You can activate one of the planeswalker's loyalty abilities the turn it enters the battlefield." 56 | }, 57 | { 58 | "date": "2018-07-13", 59 | "text": "In some rare cases, a spell or ability may cause Nicol Bolas to transform while it's a creature on the battlefield. If this happens, the resulting planeswalker won't have any loyalty counters on it and will subsequently be put into its owner's graveyard." 60 | } 61 | ], 62 | "foreignNames": [ 63 | { 64 | "name": "Nicol Bolas, der Emporgestiegene", 65 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=447636&type=card", 66 | "language": "German", 67 | "multiverseid": 447636 68 | }, 69 | { 70 | "name": "Nicol Bolas, el Resurgido", 71 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=447917&type=card", 72 | "language": "Spanish", 73 | "multiverseid": 447917 74 | }, 75 | { 76 | "name": "Nicol Bolas, le transcendé", 77 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=448198&type=card", 78 | "language": "French", 79 | "multiverseid": 448198 80 | }, 81 | { 82 | "name": "Nicol Bolas, l'Asceso", 83 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=448479&type=card", 84 | "language": "Italian", 85 | "multiverseid": 448479 86 | }, 87 | { 88 | "name": "覚醒の龍、ニコル・ボーラス", 89 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=448760&type=card", 90 | "language": "Japanese", 91 | "multiverseid": 448760 92 | }, 93 | { 94 | "name": "부활자, 니콜 볼라스", 95 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=449041&type=card", 96 | "language": "Korean", 97 | "multiverseid": 449041 98 | }, 99 | { 100 | "name": "Nicol Bolas, o Erguido", 101 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=449322&type=card", 102 | "language": "Portuguese (Brazil)", 103 | "multiverseid": 449322 104 | }, 105 | { 106 | "name": "Никол Болас, Возвысившийся", 107 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=449603&type=card", 108 | "language": "Russian", 109 | "multiverseid": 449603 110 | }, 111 | { 112 | "name": "飞升尼可波拉斯", 113 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=449884&type=card", 114 | "language": "Chinese Simplified", 115 | "multiverseid": 449884 116 | }, 117 | { 118 | "name": "飛昇尼可波拉斯", 119 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=450165&type=card", 120 | "language": "Chinese Traditional", 121 | "multiverseid": 450165 122 | } 123 | ], 124 | "printings": [ 125 | "M19" 126 | ], 127 | "originalText": "+2: Draw two cards.\n−3: Nicol Bolas, the Arisen deals 10 damage to target creature or planeswalker.\n−4: Put target creature or planeswalker card from a graveyard onto the battlefield under your control.\n−12: Exile all but the bottom card of target player's library.", 128 | "originalType": "Legendary Planeswalker — Bolas", 129 | "legalities": [ 130 | { 131 | "format": "Brawl", 132 | "legality": "Legal" 133 | }, 134 | { 135 | "format": "Commander", 136 | "legality": "Legal" 137 | }, 138 | { 139 | "format": "Legacy", 140 | "legality": "Legal" 141 | }, 142 | { 143 | "format": "Modern", 144 | "legality": "Legal" 145 | }, 146 | { 147 | "format": "Vintage", 148 | "legality": "Legal" 149 | } 150 | ], 151 | "id": "e84c9aff1e488946b2a59e4e6b4d6dea4c49e63c" 152 | } 153 | ] 154 | } -------------------------------------------------------------------------------- /stubby/json/types/subtypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "subtypes": [ 3 | "Advisor", 4 | "Aetherborn", 5 | "Ajani", 6 | "Alara", 7 | "Ally", 8 | "Angel", 9 | "Antelope", 10 | "Ape", 11 | "Arcane", 12 | "Archer", 13 | "Archon", 14 | "Arkhos", 15 | "Arlinn", 16 | "Artificer", 17 | "Ashiok", 18 | "Assassin", 19 | "Assembly-Worker", 20 | "Atog", 21 | "Aura", 22 | "Aurochs", 23 | "Avatar", 24 | "Azgol", 25 | "Baddest,", 26 | "Badger", 27 | "Barbarian", 28 | "Basilisk", 29 | "Bat", 30 | "Bear", 31 | "Beast", 32 | "Beeble", 33 | "Belenon", 34 | "Berserker", 35 | "Biggest,", 36 | "Bird", 37 | "Boar", 38 | "Bolas", 39 | "Bolas\u2019s Meditation Realm", 40 | "Bringer", 41 | "Brushwagg", 42 | "Bureaucrat", 43 | "Camel", 44 | "Carrier", 45 | "Cat", 46 | "Centaur", 47 | "Cephalid", 48 | "Chandra", 49 | "Chicken", 50 | "Child", 51 | "Chimera", 52 | "Clamfolk", 53 | "Cleric", 54 | "Cockatrice", 55 | "Construct", 56 | "Cow", 57 | "Crab", 58 | "Crocodile", 59 | "Curse", 60 | "Cyclops", 61 | "Dack", 62 | "Daretti", 63 | "Dauthi", 64 | "Demon", 65 | "Desert", 66 | "Designer", 67 | "Devil", 68 | "Dinosaur", 69 | "Djinn", 70 | "Dominaria", 71 | "Domri", 72 | "Donkey", 73 | "Dovin", 74 | "Dragon", 75 | "Drake", 76 | "Dreadnought", 77 | "Drone", 78 | "Druid", 79 | "Dryad", 80 | "Dwarf", 81 | "Efreet", 82 | "Egg", 83 | "Elder", 84 | "Eldrazi", 85 | "Elemental", 86 | "Elephant", 87 | "Elf", 88 | "Elk", 89 | "Elspeth", 90 | "Elves", 91 | "Equilor", 92 | "Equipment", 93 | "Ergamon", 94 | "Etiquette", 95 | "Eye", 96 | "Fabacin", 97 | "Faerie", 98 | "Ferret", 99 | "Fish", 100 | "Flagbearer", 101 | "Forest", 102 | "Fortification", 103 | "Fox", 104 | "Freyalise", 105 | "Frog", 106 | "Fungus", 107 | "Gamer", 108 | "Gargoyle", 109 | "Garruk", 110 | "Gate", 111 | "Giant", 112 | "Gideon", 113 | "Gnome", 114 | "Goat", 115 | "Goblin", 116 | "Goblins", 117 | "God", 118 | "Golem", 119 | "Gorgon", 120 | "Gremlin", 121 | "Griffin", 122 | "Gus", 123 | "Hag", 124 | "Harpy", 125 | "Hellion", 126 | "Hero", 127 | "Hippo", 128 | "Hippogriff", 129 | "Homarid", 130 | "Homunculus", 131 | "Horror", 132 | "Horse", 133 | "Hound", 134 | "Human", 135 | "Hydra", 136 | "Hyena", 137 | "Igpay", 138 | "Illusion", 139 | "Imp", 140 | "Incarnation", 141 | "Innistrad", 142 | "Insect", 143 | "Iquatana", 144 | "Ir", 145 | "Island", 146 | "Jace", 147 | "Jellyfish", 148 | "Juggernaut", 149 | "Kaldheim", 150 | "Kamigawa", 151 | "Karn", 152 | "Karsus", 153 | "Kavu", 154 | "Kaya", 155 | "Kephalai", 156 | "Kinshala", 157 | "Kiora", 158 | "Kirin", 159 | "Kithkin", 160 | "Knight", 161 | "Kobold", 162 | "Kolbahan", 163 | "Kor", 164 | "Koth", 165 | "Kraken", 166 | "Kyneth", 167 | "Lady", 168 | "Lair", 169 | "Lamia", 170 | "Lammasu", 171 | "Leech", 172 | "Legend", 173 | "Leviathan", 174 | "Lhurgoyf", 175 | "Licid", 176 | "Liliana", 177 | "Lizard", 178 | "Locus", 179 | "Lord", 180 | "Lorwyn", 181 | "Luvion", 182 | "Manticore", 183 | "Masticore", 184 | "Mercadia", 185 | "Mercenary", 186 | "Merfolk", 187 | "Metathran", 188 | "Mime", 189 | "Mine", 190 | "Minion", 191 | "Minotaur", 192 | "Mirrodin", 193 | "Moag", 194 | "Mole", 195 | "Monger", 196 | "Mongoose", 197 | "Mongseng", 198 | "Monk", 199 | "Monkey", 200 | "Moonfolk", 201 | "Mountain", 202 | "Mummy", 203 | "Muraganda", 204 | "Mutant", 205 | "Myr", 206 | "Mystic", 207 | "Naga", 208 | "Nahiri", 209 | "Narset", 210 | "Nastiest,", 211 | "Nautilus", 212 | "Nephilim", 213 | "New Phyrexia", 214 | "Nightmare", 215 | "Nightstalker", 216 | "Ninja", 217 | "Nissa", 218 | "Nixilis", 219 | "Noggle", 220 | "Nomad", 221 | "Nymph", 222 | "Octopus", 223 | "Ogre", 224 | "Ooze", 225 | "Orc", 226 | "Orgg", 227 | "Ouphe", 228 | "Ox", 229 | "Oyster", 230 | "Paratrooper", 231 | "Pegasus", 232 | "Pest", 233 | "Phelddagrif", 234 | "Phoenix", 235 | "Phyrexia", 236 | "Pilot", 237 | "Pirate", 238 | "Plains", 239 | "Plant", 240 | "Power-Plant", 241 | "Praetor", 242 | "Processor", 243 | "Proper", 244 | "Pyrulea", 245 | "Rabbit", 246 | "Rabiah", 247 | "Ral", 248 | "Rat", 249 | "Rath", 250 | "Ravnica", 251 | "Rebel", 252 | "Reflection", 253 | "Regatha", 254 | "Rhino", 255 | "Rigger", 256 | "Rogue", 257 | "Sable", 258 | "Saheeli", 259 | "Salamander", 260 | "Samurai", 261 | "Saproling", 262 | "Sarkhan", 263 | "Satyr", 264 | "Scarecrow", 265 | "Scion", 266 | "Scorpion", 267 | "Scout", 268 | "Segovia", 269 | "Serpent", 270 | "Serra\u2019s Realm", 271 | "Shade", 272 | "Shadowmoor", 273 | "Shaman", 274 | "Shandalar", 275 | "Shapeshifter", 276 | "Sheep", 277 | "Ship", 278 | "Shrine", 279 | "Siren", 280 | "Skeleton", 281 | "Slith", 282 | "Sliver", 283 | "Slug", 284 | "Snake", 285 | "Soldier", 286 | "Soltari", 287 | "Sorin", 288 | "Spawn", 289 | "Specter", 290 | "Spellshaper", 291 | "Sphinx", 292 | "Spider", 293 | "Spike", 294 | "Spirit", 295 | "Sponge", 296 | "Squid", 297 | "Squirrel", 298 | "Starfish", 299 | "Surrakar", 300 | "Swamp", 301 | "Tamiyo", 302 | "Teferi", 303 | "Tezzeret", 304 | "Thalakos", 305 | "The", 306 | "Thopter", 307 | "Thrull", 308 | "Tibalt", 309 | "Tower", 310 | "Townsfolk", 311 | "Trap", 312 | "Treefolk", 313 | "Troll", 314 | "Turtle", 315 | "Ugin", 316 | "Ulgrotha", 317 | "Unicorn", 318 | "Urza\u2019s", 319 | "Valla", 320 | "Vampire", 321 | "Vedalken", 322 | "Vehicle", 323 | "Venser", 324 | "Viashino", 325 | "Volver", 326 | "Vraska", 327 | "Vryn", 328 | "Waiter", 329 | "Wall", 330 | "Warrior", 331 | "Weird", 332 | "Werewolf", 333 | "Whale", 334 | "Wildfire", 335 | "Wizard", 336 | "Wolf", 337 | "Wolverine", 338 | "Wombat", 339 | "Worm", 340 | "Wraith", 341 | "Wurm", 342 | "Xenagos", 343 | "Xerex", 344 | "Yeti", 345 | "Zendikar", 346 | "Zombie", 347 | "Zubera" 348 | ] 349 | } -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/api/MTGAPI.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.api; 2 | 3 | import io.magicthegathering.javasdk.exception.HttpRequestFailedException; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | import okhttp3.OkHttpClient; 13 | import okhttp3.Request; 14 | import okhttp3.Response; 15 | 16 | import com.google.gson.Gson; 17 | import com.google.gson.GsonBuilder; 18 | import com.google.gson.JsonElement; 19 | import com.google.gson.JsonObject; 20 | 21 | /** 22 | * Base class for using the magicthegathering.io APIs. 23 | * 24 | * @author thechucklingatom 25 | * @author nniklas 26 | */ 27 | public abstract class MTGAPI { 28 | protected static String ENDPOINT = "https://api.magicthegathering.io/v1"; 29 | private static long connectTimeout = 10; 30 | private static long writeTimeout = 10; 31 | private static long readTimeout = 60; 32 | 33 | private static OkHttpClient client; // Lazy initialization of client, so is not created twice, if a timeout is set. 34 | private static String DELIM_LINK = ","; 35 | private static String DELIM_LINK_PARAM = ";"; 36 | 37 | /** 38 | * @return The HTTP-Client to use for new connections 39 | */ 40 | private static OkHttpClient getClient() { 41 | if (client == null) { 42 | recreateClient(); 43 | } 44 | return client; 45 | } 46 | 47 | /** 48 | * @return The connectTimeout for the HTTP-Client 49 | * @see OkHttpClient.Builder#connectTimeout(long, TimeUnit) 50 | */ 51 | public static long getConnectTimeout() { 52 | return connectTimeout; 53 | } 54 | 55 | /** 56 | * Sets the {@link OkHttpClient.Builder#connectTimeout(long, TimeUnit)} to the specified value and creates a new client using it 57 | * @param connectTimeout The timeout to set 58 | */ 59 | public static void setConnectTimeout(long connectTimeout) { 60 | MTGAPI.connectTimeout = connectTimeout; 61 | if (client != null) { // Only change the timeout on the client, if the client was already created. 62 | recreateClient(); 63 | } 64 | } 65 | 66 | /** 67 | * @return The writeTimeout for the HTTP-Client 68 | * @see OkHttpClient.Builder#writeTimeout(long, TimeUnit) 69 | */ 70 | public static long getWriteTimeout() { 71 | return writeTimeout; 72 | } 73 | 74 | /** 75 | * Sets the {@link OkHttpClient.Builder#writeTimeout(long, TimeUnit)} to the specified value and creates a new client using it 76 | * @param writeTimeout The timeout to set 77 | */ 78 | public static void setWriteTimeout(long writeTimeout) { 79 | MTGAPI.writeTimeout = writeTimeout; 80 | if (client != null) { // Only change the timeout on the client, if the client was already created. 81 | recreateClient(); 82 | } 83 | } 84 | 85 | /** 86 | * @return The readTimeout for the HTTP-Client 87 | * @see OkHttpClient.Builder#writeTimeout(long, TimeUnit) 88 | */ 89 | public static long getReadTimeout() { 90 | return readTimeout; 91 | } 92 | 93 | /** 94 | * Sets the {@link OkHttpClient.Builder#readTimeout(long, TimeUnit)} to the specified value and creates a new client using it 95 | * @param readTimeout The timeout to set 96 | */ 97 | public static void setReadTimeout(long readTimeout) { 98 | MTGAPI.readTimeout = readTimeout; 99 | if (client != null) { // Only change the timeout on the client, if the client was already created. 100 | recreateClient(); 101 | } 102 | } 103 | 104 | /** 105 | * Creates a new {@link OkHttpClient.Builder}, sets the timeouts and builds a new {@link OkHttpClient} with it. 106 | */ 107 | private static void recreateClient() { 108 | OkHttpClient.Builder builder; 109 | if (client == null) { 110 | builder = new OkHttpClient.Builder(); 111 | } else { 112 | builder = client.newBuilder(); 113 | } 114 | client = builder.connectTimeout(connectTimeout, TimeUnit.SECONDS) 115 | .writeTimeout(writeTimeout, TimeUnit.SECONDS) 116 | .readTimeout(readTimeout, TimeUnit.SECONDS) 117 | .build(); 118 | } 119 | 120 | /** 121 | * Make an HTTP GET request to the given path and map the object under the 122 | * given key in the JSON of the response to the Java {@link Class} of type 123 | * {@link TYPE}. 124 | * 125 | * @param path Controlled by what is calling the function, currently "sets" or "cards" 126 | * depending on the calling class. 127 | * @param key The key for the JSON element we are looking for. 128 | * @param expectedClass The class type we are expecting to get back. 129 | * @return Object of the requested type if found. 130 | */ 131 | protected static TYPE get(String path, String key, 132 | Class expectedClass) { 133 | Gson deserializer = new GsonBuilder().create(); 134 | JsonObject jsonObject = getJsonObject(path, deserializer).get(0); 135 | return deserializer.fromJson(jsonObject.get(key), 136 | expectedClass); 137 | } 138 | 139 | /** 140 | * Make an HTTP GET request to the given path and map the array under the 141 | * given key in the JSON of the response to a {@link List} of type 142 | * {@link TYPE}. 143 | * 144 | * @param path Controlled by what is calling the function, currently "sets" or "cards" 145 | * depending on the calling class. 146 | * @param key The key for the JSON element we are looking for. 147 | * @param expectedClass The class type we are expecting to get back. 148 | * @return a {@link List} of {@link TYPE} 149 | */ 150 | protected static List getList(String path, String key, 151 | Class expectedClass) { 152 | Gson deserializer = new GsonBuilder().create(); 153 | List toReturn = new ArrayList<>(); 154 | List jsonObjectList = getJsonObject(path, deserializer); 155 | 156 | for(JsonObject jsonObject : jsonObjectList) { 157 | for (JsonElement jsonElement : 158 | jsonObject.get(key).getAsJsonArray()) { 159 | toReturn.add(deserializer.fromJson( 160 | jsonElement, expectedClass)); 161 | } 162 | } 163 | 164 | return toReturn; 165 | } 166 | 167 | /** 168 | * Deserialize the object to the type expected. 169 | * 170 | * @param path Controlled by what is calling the function, currently "sets" or "cards" 171 | * depending on the calling class. 172 | * @param deserializer {@link Gson} object that will be used to deserialize the JSON returned 173 | * from the web API. 174 | * @return The parsed {@link JsonObject} 175 | */ 176 | private static List getJsonObject(String path, Gson deserializer) { 177 | String url = String.format("%s/%s", ENDPOINT, path); 178 | Request request = new Request.Builder().url(url).build(); 179 | Response response; 180 | try { 181 | response = getClient().newCall(request).execute(); 182 | 183 | ArrayList objectList = new ArrayList<>(); 184 | String linkHeader = response.headers().get("Link"); 185 | if (linkHeader == null || linkHeader.isEmpty() || path.contains("page=")) { 186 | objectList.add(deserializer.fromJson(response.body() 187 | .string(), JsonObject.class)); 188 | return objectList; 189 | } else { 190 | int numberOfPages = 0; 191 | String[] linkStrings = linkHeader.split(DELIM_LINK); 192 | List paramList = new ArrayList<>(); 193 | for (String link : linkStrings) { 194 | paramList.add(link.split(DELIM_LINK_PARAM)); 195 | } 196 | for (String[] params : paramList) { 197 | if (params[1].contains("last")) { 198 | Matcher matcher = Pattern.compile("page=[0-9]+").matcher(params[0]); 199 | numberOfPages = (matcher.find()) ? Integer.parseInt(matcher.group().substring(5)) : 0; 200 | } 201 | } 202 | 203 | objectList.add(deserializer.fromJson(response.body().string(), JsonObject.class)); 204 | 205 | if (!url.contains("?")) { 206 | url += "?"; 207 | } 208 | 209 | for(int i = 2; i <= numberOfPages; i++){ 210 | request = new Request.Builder().url(url + "&page=" + i).build(); 211 | response = getClient().newCall(request).execute(); 212 | objectList.add(deserializer.fromJson(response.body().string(), JsonObject.class)); 213 | } 214 | 215 | return objectList; 216 | } 217 | } catch (IOException e) { 218 | throw new HttpRequestFailedException(e); 219 | } 220 | 221 | } 222 | 223 | /** 224 | * Get the list of objects with a filter if there is anything that matches the filters. 225 | * 226 | * @param path Controlled by what is calling the function, currently "sets" or "cards" 227 | * depending on the calling class. 228 | * @param key The key for the JSON element we are looking for. 229 | * @param expectedClass The class type we are expecting to get back. 230 | * @param filters List of filters we want to apply to the search, you can see a full list of 231 | * accepted filters at the web API docs 232 | * @return List of found cards. 233 | */ 234 | protected static List getList(String path, String key, 235 | Class expectedClass, List filters) { 236 | StringBuilder tempPath = new StringBuilder(path); 237 | tempPath.append("?"); 238 | 239 | for (String filter : 240 | filters) { 241 | tempPath.append(filter).append('&'); 242 | } 243 | 244 | return getList(tempPath.substring(0, tempPath.length() - 1), key, expectedClass); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/main/java/io/magicthegathering/javasdk/resource/Card.java: -------------------------------------------------------------------------------- 1 | package io.magicthegathering.javasdk.resource; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigDecimal; 5 | 6 | /** 7 | * This file is part of mtgsdk. 8 | * https://github.com/MagicTheGathering/mtg-sdk-java 9 | *

10 | * Licensed under the MIT license: 11 | * http://www.opensource.org/licenses/MIT-license 12 | *

13 | * Created by thechucklingatom on 2/16/2016. 14 | *

15 | * Card class that is created from the JSON set representation. 16 | * 17 | * @author thechucklingatom 18 | */ 19 | @SuppressWarnings("serial") 20 | public class Card implements Serializable { 21 | private String id; 22 | private String layout; 23 | private String name; 24 | private String[] names; 25 | private String manaCost; 26 | private double cmc; 27 | private String[] colors; 28 | private String[] colorIdentity; 29 | private String type; 30 | private String[] supertypes; 31 | private String[] types; 32 | private String[] subtypes; 33 | private String rarity; 34 | private String text; 35 | private String originalText; 36 | private String flavor; 37 | private String artist; 38 | private String number; 39 | private String power; 40 | private String toughness; 41 | private String loyalty; 42 | private int multiverseid = -1; 43 | private String[] variations; 44 | private String imageName; 45 | private String watermark; 46 | private String border; 47 | private boolean timeshifted; 48 | private int hand; 49 | private int life; 50 | private boolean reserved; 51 | private String releaseDate; 52 | private boolean starter; 53 | private String set; 54 | private String setName; 55 | private String[] printings; 56 | private String imageUrl; 57 | private Legality[] legalities; 58 | private BigDecimal priceHigh; 59 | private BigDecimal priceMid; 60 | private BigDecimal priceLow; 61 | private BigDecimal onlinePriceHigh; 62 | private BigDecimal onlinePriceMid; 63 | private BigDecimal onlinePriceLow; 64 | private Ruling[] rulings; 65 | private ForeignData[] foreignNames; 66 | 67 | public String getId() { 68 | return id; 69 | } 70 | 71 | public void setId(String id) { 72 | this.id = id; 73 | } 74 | 75 | public String getLayout() { 76 | return layout; 77 | } 78 | 79 | public void setLayout(String layout) { 80 | this.layout = layout; 81 | } 82 | 83 | public String getName() { 84 | return name; 85 | } 86 | 87 | public void setName(String name) { 88 | this.name = name; 89 | } 90 | 91 | public String[] getNames() { 92 | return names; 93 | } 94 | 95 | public void setNames(String[] names) { 96 | this.names = names; 97 | } 98 | 99 | public String getManaCost() { 100 | return manaCost; 101 | } 102 | 103 | public void setManaCost(String manaCost) { 104 | this.manaCost = manaCost; 105 | } 106 | 107 | public double getCmc() { 108 | return cmc; 109 | } 110 | 111 | public void setCmc(double cmc) { 112 | this.cmc = cmc; 113 | } 114 | 115 | public String[] getColors() { 116 | return colors; 117 | } 118 | 119 | public void setColors(String[] colors) { 120 | this.colors = colors; 121 | } 122 | 123 | public String[] getColorIdentity() { 124 | return colorIdentity; 125 | } 126 | 127 | public void setColorIdentity(String[] colorIdentity) { 128 | this.colorIdentity = colorIdentity; 129 | } 130 | 131 | public String getType() { 132 | return type; 133 | } 134 | 135 | public void setType(String type) { 136 | this.type = type; 137 | } 138 | 139 | public String[] getSupertypes() { 140 | return supertypes; 141 | } 142 | 143 | public void setSupertypes(String[] supertypes) { 144 | this.supertypes = supertypes; 145 | } 146 | 147 | public String[] getTypes() { 148 | return types; 149 | } 150 | 151 | public void setTypes(String[] types) { 152 | this.types = types; 153 | } 154 | 155 | public String[] getSubtypes() { 156 | return subtypes; 157 | } 158 | 159 | public void setSubtypes(String[] subtypes) { 160 | this.subtypes = subtypes; 161 | } 162 | 163 | public String getRarity() { 164 | return rarity; 165 | } 166 | 167 | public void setRarity(String rarity) { 168 | this.rarity = rarity; 169 | } 170 | 171 | public String getText() { 172 | return text; 173 | } 174 | 175 | public void setText(String text) { 176 | this.text = text; 177 | } 178 | 179 | public String getFlavor() { 180 | return flavor; 181 | } 182 | 183 | public void setFlavor(String flavor) { 184 | this.flavor = flavor; 185 | } 186 | 187 | public String getArtist() { 188 | return artist; 189 | } 190 | 191 | public void setArtist(String artist) { 192 | this.artist = artist; 193 | } 194 | 195 | public String getNumber() { 196 | return number; 197 | } 198 | 199 | public void setNumber(String number) { 200 | this.number = number; 201 | } 202 | 203 | public String getPower() { 204 | return power; 205 | } 206 | 207 | public void setPower(String power) { 208 | this.power = power; 209 | } 210 | 211 | public String getToughness() { 212 | return toughness; 213 | } 214 | 215 | public void setToughness(String toughness) { 216 | this.toughness = toughness; 217 | } 218 | 219 | public String getLoyalty() { 220 | return loyalty; 221 | } 222 | 223 | public void setLoyalty(String loyalty) { 224 | this.loyalty = loyalty; 225 | } 226 | 227 | public int getMultiverseid() { 228 | return multiverseid; 229 | } 230 | 231 | public void setMultiverseid(int multiverseid) { 232 | this.multiverseid = multiverseid; 233 | } 234 | 235 | public String[] getVariations() { 236 | return variations; 237 | } 238 | 239 | public void setVariations(String[] variations) { 240 | this.variations = variations; 241 | } 242 | 243 | public String getImageName() { 244 | return imageName; 245 | } 246 | 247 | public void setImageName(String imageName) { 248 | this.imageName = imageName; 249 | } 250 | 251 | public String getWatermark() { 252 | return watermark; 253 | } 254 | 255 | public void setWatermark(String watermark) { 256 | this.watermark = watermark; 257 | } 258 | 259 | public String getBorder() { 260 | return border; 261 | } 262 | 263 | public void setBorder(String border) { 264 | this.border = border; 265 | } 266 | 267 | public boolean isTimeshifted() { 268 | return timeshifted; 269 | } 270 | 271 | public void setTimeshifted(boolean timeshifted) { 272 | this.timeshifted = timeshifted; 273 | } 274 | 275 | public int getHand() { 276 | return hand; 277 | } 278 | 279 | public void setHand(int hand) { 280 | this.hand = hand; 281 | } 282 | 283 | public int getLife() { 284 | return life; 285 | } 286 | 287 | public void setLife(int life) { 288 | this.life = life; 289 | } 290 | 291 | public boolean isReserved() { 292 | return reserved; 293 | } 294 | 295 | public void setReserved(boolean reserved) { 296 | this.reserved = reserved; 297 | } 298 | 299 | public String getReleaseDate() { 300 | return releaseDate; 301 | } 302 | 303 | public void setReleaseDate(String releaseDate) { 304 | this.releaseDate = releaseDate; 305 | } 306 | 307 | public boolean isStarter() { 308 | return starter; 309 | } 310 | 311 | public void setStarter(boolean starter) { 312 | this.starter = starter; 313 | } 314 | 315 | /** 316 | * dirty compare to in order to start testing. Just comparing the 317 | * MultiverseId which should be unique. 318 | * 319 | * @param toCompare A {@link Card} object hopefully 320 | * @return true if the same set, false if different. 321 | */ 322 | @Override 323 | public boolean equals(Object toCompare) { 324 | if (toCompare instanceof Card) { 325 | Card cardCompare = (Card) toCompare; 326 | return getMultiverseid() == cardCompare.getMultiverseid() 327 | && getName().equals(cardCompare.getName()) 328 | && getCmc() == cardCompare.getCmc(); 329 | } else { 330 | return false; 331 | } 332 | } 333 | 334 | /** 335 | * Prints the Card name and multiverseId which should give enough info for 336 | * debug testing. 337 | * 338 | * @return The cards name and Id 339 | */ 340 | @Override 341 | public String toString() { 342 | return "\nCard Name: " + getName() + 343 | "\nMultiverse Id: " + getMultiverseid() + 344 | "\nMana Cost: " + getManaCost(); 345 | } 346 | 347 | public String getSet() { 348 | return set; 349 | } 350 | 351 | public void setSet(String set) { 352 | this.set = set; 353 | } 354 | 355 | public String getSetName() { 356 | return setName; 357 | } 358 | 359 | public void setSetName(String setName) { 360 | this.setName = setName; 361 | } 362 | 363 | public String[] getPrintings() { 364 | return printings; 365 | } 366 | 367 | public void setPrintings(String[] printings) { 368 | this.printings = printings; 369 | } 370 | 371 | public String getOriginalText() { 372 | return originalText; 373 | } 374 | 375 | public void setOriginalText(String originalText) { 376 | this.originalText = originalText; 377 | } 378 | 379 | public String getImageUrl() { 380 | return imageUrl; 381 | } 382 | 383 | public void setImageUrl(String imageUrl) { 384 | this.imageUrl = imageUrl; 385 | } 386 | 387 | public Legality[] getLegalities() { 388 | return legalities; 389 | } 390 | 391 | public void setLegalities(Legality[] legalities) { 392 | this.legalities = legalities; 393 | } 394 | 395 | public BigDecimal getPriceHigh() { 396 | return priceHigh; 397 | } 398 | 399 | public void setPriceHigh(BigDecimal priceHigh) { 400 | this.priceHigh = priceHigh; 401 | } 402 | 403 | public BigDecimal getPriceMid() { 404 | return priceMid; 405 | } 406 | 407 | public void setPriceMid(BigDecimal priceMid) { 408 | this.priceMid = priceMid; 409 | } 410 | 411 | public BigDecimal getPriceLow() { 412 | return priceLow; 413 | } 414 | 415 | public void setPriceLow(BigDecimal priceLow) { 416 | this.priceLow = priceLow; 417 | } 418 | 419 | public BigDecimal getOnlinePriceHigh() { 420 | return onlinePriceHigh; 421 | } 422 | 423 | public void setOnlinePriceHigh(BigDecimal onlinePriceHigh) { 424 | this.onlinePriceHigh = onlinePriceHigh; 425 | } 426 | 427 | public BigDecimal getOnlinePriceMid() { 428 | return onlinePriceMid; 429 | } 430 | 431 | public void setOnlinePriceMid(BigDecimal onlinePriceMid) { 432 | this.onlinePriceMid = onlinePriceMid; 433 | } 434 | 435 | public BigDecimal getOnlinePriceLow() { 436 | return onlinePriceLow; 437 | } 438 | 439 | public void setOnlinePriceLow(BigDecimal onlinePriceLow) { 440 | this.onlinePriceLow = onlinePriceLow; 441 | } 442 | 443 | public Ruling[] getRulings() { 444 | return rulings; 445 | } 446 | 447 | public void setRulings(Ruling[] rulings) { 448 | this.rulings = rulings; 449 | } 450 | 451 | public ForeignData[] getForeignNames() { 452 | return foreignNames; 453 | } 454 | 455 | public void setForeignNames(ForeignData[] foreignNames) { 456 | this.foreignNames = foreignNames; 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /stubby/json/cards/DRKpage2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cards": [ 3 | { 4 | "name": "Festival", 5 | "manaCost": "{W}", 6 | "cmc": 1, 7 | "colors": [ 8 | "White" 9 | ], 10 | "colorIdentity": [ 11 | "W" 12 | ], 13 | "type": "Instant", 14 | "types": [ 15 | "Instant" 16 | ], 17 | "rarity": "Common", 18 | "set": "DRK", 19 | "setName": "The Dark", 20 | "text": "Cast Festival only during an opponent's upkeep.\nCreatures can't attack this turn.", 21 | "flavor": "Only after the townsfolk had drawn us into their merry celebration did we discover that their holiday rituals held a deeper purpose.", 22 | "artist": "Mark Poole", 23 | "layout": "normal", 24 | "multiverseid": 1808, 25 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1808&type=card", 26 | "printings": [ 27 | "DRK" 28 | ], 29 | "originalText": "Opponent may not declare an attack this turn. Play during opponent's upkeep phase.", 30 | "originalType": "Instant", 31 | "legalities": [ 32 | { 33 | "format": "Commander", 34 | "legality": "Legal" 35 | }, 36 | { 37 | "format": "Legacy", 38 | "legality": "Legal" 39 | }, 40 | { 41 | "format": "Vintage", 42 | "legality": "Legal" 43 | } 44 | ], 45 | "id": "388c616b787db1b1df20d6cc9f64fcad7033b354" 46 | }, 47 | { 48 | "name": "Fire and Brimstone", 49 | "manaCost": "{3}{W}{W}", 50 | "cmc": 5, 51 | "colors": [ 52 | "White" 53 | ], 54 | "colorIdentity": [ 55 | "W" 56 | ], 57 | "type": "Instant", 58 | "types": [ 59 | "Instant" 60 | ], 61 | "rarity": "Uncommon", 62 | "set": "DRK", 63 | "setName": "The Dark", 64 | "text": "Fire and Brimstone deals 4 damage to target player who declared an attacking creature this turn and 4 damage to you.", 65 | "artist": "Jeff A. Menges", 66 | "layout": "normal", 67 | "multiverseid": 1809, 68 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1809&type=card", 69 | "printings": [ 70 | "DRK" 71 | ], 72 | "originalText": "Fire and Brimstone does 4 damage to target player and 4 damage to you. Can only be used during a turn in which target player has declared an attack.", 73 | "originalType": "Instant", 74 | "legalities": [ 75 | { 76 | "format": "Commander", 77 | "legality": "Legal" 78 | }, 79 | { 80 | "format": "Legacy", 81 | "legality": "Legal" 82 | }, 83 | { 84 | "format": "Vintage", 85 | "legality": "Legal" 86 | } 87 | ], 88 | "id": "62dffa0a469fcbf6d70187bdd3a59101d2105e53" 89 | }, 90 | { 91 | "name": "Holy Light", 92 | "manaCost": "{2}{W}", 93 | "cmc": 3, 94 | "colors": [ 95 | "White" 96 | ], 97 | "colorIdentity": [ 98 | "W" 99 | ], 100 | "type": "Instant", 101 | "types": [ 102 | "Instant" 103 | ], 104 | "rarity": "Common", 105 | "set": "DRK", 106 | "setName": "The Dark", 107 | "text": "Nonwhite creatures get -1/-1 until end of turn.", 108 | "flavor": "\"Bathed in hallowed light, the infidels looked upon the impurities of their souls and despaired.\" —The Book of Tal", 109 | "artist": "Drew Tucker", 110 | "layout": "normal", 111 | "multiverseid": 1810, 112 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1810&type=card", 113 | "printings": [ 114 | "DRK", 115 | "MED" 116 | ], 117 | "originalText": "All non-white creatures get -1/-1 until end of turn.", 118 | "originalType": "Instant", 119 | "legalities": [ 120 | { 121 | "format": "Commander", 122 | "legality": "Legal" 123 | }, 124 | { 125 | "format": "Legacy", 126 | "legality": "Legal" 127 | }, 128 | { 129 | "format": "Vintage", 130 | "legality": "Legal" 131 | } 132 | ], 133 | "id": "a2e1173a2a34131e41ce8029beb43841724610cc" 134 | }, 135 | { 136 | "name": "Knights of Thorn", 137 | "manaCost": "{3}{W}", 138 | "cmc": 4, 139 | "colors": [ 140 | "White" 141 | ], 142 | "colorIdentity": [ 143 | "W" 144 | ], 145 | "type": "Creature — Human Knight", 146 | "types": [ 147 | "Creature" 148 | ], 149 | "subtypes": [ 150 | "Human", 151 | "Knight" 152 | ], 153 | "rarity": "Rare", 154 | "set": "DRK", 155 | "setName": "The Dark", 156 | "text": "Protection from red; banding (Any creatures with banding, and up to one without, can attack in a band. Bands are blocked as a group. If any creatures with banding you control are blocking or being blocked by a creature, you divide that creature's combat damage, not its controller, among any of the creatures it's being blocked by or is blocking.)", 157 | "flavor": "\"With a great cry, the Goblin host broke and ran as the first wave of Knights penetrated its ranks.\" —Tivadar of Thorn, History of the Goblin Wars", 158 | "artist": "Christopher Rush", 159 | "power": "2", 160 | "toughness": "2", 161 | "layout": "normal", 162 | "multiverseid": 1811, 163 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1811&type=card", 164 | "reserved": true, 165 | "rulings": [ 166 | { 167 | "date": "2008-10-01", 168 | "text": "If a creature with banding attacks, it can team up with any number of other attacking creatures with banding (and up to one nonbanding creature) and attack as a unit called a \"band.\" The band can be blocked by any creature that could block a single creature in the band. Blocking any creature in a band blocks the entire band. If a creature with banding is blocked, the attacking player chooses how the blockers' damage is assigned." 169 | }, 170 | { 171 | "date": "2008-10-01", 172 | "text": "A maximum of one nonbanding creature can join an attacking band no matter how many creatures with banding are in it." 173 | }, 174 | { 175 | "date": "2008-10-01", 176 | "text": "Creatures in the same band must all attack the same player or planeswalker." 177 | }, 178 | { 179 | "date": "2009-10-01", 180 | "text": "If a creature in combat has banding, its controller assigns damage for creatures blocking or blocked by it. That player can ignore the damage assignment order when making this assignment." 181 | } 182 | ], 183 | "printings": [ 184 | "DRK", 185 | "MED" 186 | ], 187 | "originalText": "Protection from red, banding", 188 | "originalType": "Summon — Knights", 189 | "legalities": [ 190 | { 191 | "format": "Commander", 192 | "legality": "Legal" 193 | }, 194 | { 195 | "format": "Legacy", 196 | "legality": "Legal" 197 | }, 198 | { 199 | "format": "Vintage", 200 | "legality": "Legal" 201 | } 202 | ], 203 | "id": "437d948076044881153945e80dad1dc8314b202b" 204 | }, 205 | { 206 | "name": "Martyr's Cry", 207 | "manaCost": "{W}{W}", 208 | "cmc": 2, 209 | "colors": [ 210 | "White" 211 | ], 212 | "colorIdentity": [ 213 | "W" 214 | ], 215 | "type": "Sorcery", 216 | "types": [ 217 | "Sorcery" 218 | ], 219 | "rarity": "Rare", 220 | "set": "DRK", 221 | "setName": "The Dark", 222 | "text": "Exile all white creatures. For each creature exiled this way, its controller draws a card.", 223 | "flavor": "\"It is only fitting that one such as I should die in pursuit of knowledge.\" —Vervamon the Elder", 224 | "artist": "Jeff A. Menges", 225 | "layout": "normal", 226 | "multiverseid": 1812, 227 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1812&type=card", 228 | "reserved": true, 229 | "rulings": [ 230 | { 231 | "date": "2004-10-04", 232 | "text": "Only affects creatures on the battlefield, not ones in hands or graveyards." 233 | } 234 | ], 235 | "printings": [ 236 | "DRK", 237 | "ME4" 238 | ], 239 | "originalText": "All white creatures are removed from the game. Players must draw one card for each white creature they control that is lost in this manner.", 240 | "originalType": "Sorcery", 241 | "legalities": [ 242 | { 243 | "format": "Commander", 244 | "legality": "Legal" 245 | }, 246 | { 247 | "format": "Legacy", 248 | "legality": "Legal" 249 | }, 250 | { 251 | "format": "Vintage", 252 | "legality": "Legal" 253 | } 254 | ], 255 | "id": "74078638831d1db013f61ffb72febc1050bae2bd" 256 | }, 257 | { 258 | "name": "Miracle Worker", 259 | "manaCost": "{W}", 260 | "cmc": 1, 261 | "colors": [ 262 | "White" 263 | ], 264 | "colorIdentity": [ 265 | "W" 266 | ], 267 | "type": "Creature — Human Cleric", 268 | "types": [ 269 | "Creature" 270 | ], 271 | "subtypes": [ 272 | "Human", 273 | "Cleric" 274 | ], 275 | "rarity": "Common", 276 | "set": "DRK", 277 | "setName": "The Dark", 278 | "text": "{T}: Destroy target Aura attached to a creature you control.", 279 | "flavor": "\"Those blessed hands could bring surcease to even the most tainted soul.\" —Sister Betje, Miracles of the Saints", 280 | "artist": "Ron Spencer", 281 | "power": "1", 282 | "toughness": "1", 283 | "layout": "normal", 284 | "multiverseid": 1813, 285 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1813&type=card", 286 | "printings": [ 287 | "DRK" 288 | ], 289 | "originalText": "{T}: Destroy target enchantment card on a creature you control.", 290 | "originalType": "Summon — Miracle Worker", 291 | "legalities": [ 292 | { 293 | "format": "Commander", 294 | "legality": "Legal" 295 | }, 296 | { 297 | "format": "Legacy", 298 | "legality": "Legal" 299 | }, 300 | { 301 | "format": "Vintage", 302 | "legality": "Legal" 303 | } 304 | ], 305 | "id": "8c0fe28f15cf6e452ceb8a8b4aa0c01d235cbae0" 306 | }, 307 | { 308 | "name": "Morale", 309 | "manaCost": "{1}{W}{W}", 310 | "cmc": 3, 311 | "colors": [ 312 | "White" 313 | ], 314 | "colorIdentity": [ 315 | "W" 316 | ], 317 | "type": "Instant", 318 | "types": [ 319 | "Instant" 320 | ], 321 | "rarity": "Common", 322 | "set": "DRK", 323 | "setName": "The Dark", 324 | "text": "Attacking creatures get +1/+1 until end of turn.", 325 | "flavor": "\"After Lacjsi's speech, the Knights grew determined to crush their ancient enemies clan by clan.\" —Tivadar of Thorn, History of the Goblin Wars", 326 | "artist": "Mark Poole", 327 | "layout": "normal", 328 | "multiverseid": 1814, 329 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1814&type=card", 330 | "printings": [ 331 | "DRK", 332 | "4ED" 333 | ], 334 | "originalText": "All attacking creatures gain +1/+1 until end of turn.", 335 | "originalType": "Instant", 336 | "legalities": [ 337 | { 338 | "format": "Commander", 339 | "legality": "Legal" 340 | }, 341 | { 342 | "format": "Legacy", 343 | "legality": "Legal" 344 | }, 345 | { 346 | "format": "Vintage", 347 | "legality": "Legal" 348 | } 349 | ], 350 | "id": "0319d17865def6893094955bb41c083a2c8e590c" 351 | }, 352 | { 353 | "name": "Pikemen", 354 | "manaCost": "{1}{W}", 355 | "cmc": 2, 356 | "colors": [ 357 | "White" 358 | ], 359 | "colorIdentity": [ 360 | "W" 361 | ], 362 | "type": "Creature — Human Soldier", 363 | "types": [ 364 | "Creature" 365 | ], 366 | "subtypes": [ 367 | "Human", 368 | "Soldier" 369 | ], 370 | "rarity": "Common", 371 | "set": "DRK", 372 | "setName": "The Dark", 373 | "text": "First strike; banding (Any creatures with banding, and up to one without, can attack in a band. Bands are blocked as a group. If any creatures with banding you control are blocking or being blocked by a creature, you divide that creature's combat damage, not its controller, among any of the creatures it's being blocked by or is blocking.)", 374 | "flavor": "\"As the cavalry bore down, we faced them with swords drawn and pikes hidden in the grass at our feet. 'Don't lift your pikes 'til I give the word,' I said.\" —Maeveen O'Donagh, Memoirs of a Soldier", 375 | "artist": "Dennis Detwiller", 376 | "power": "1", 377 | "toughness": "1", 378 | "layout": "normal", 379 | "multiverseid": 1815, 380 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1815&type=card", 381 | "rulings": [ 382 | { 383 | "date": "2008-10-01", 384 | "text": "If a creature with banding attacks, it can team up with any number of other attacking creatures with banding (and up to one nonbanding creature) and attack as a unit called a \"band.\" The band can be blocked by any creature that could block a single creature in the band. Blocking any creature in a band blocks the entire band. If a creature with banding is blocked, the attacking player chooses how the blockers' damage is assigned." 385 | }, 386 | { 387 | "date": "2008-10-01", 388 | "text": "A maximum of one nonbanding creature can join an attacking band no matter how many creatures with banding are in it." 389 | }, 390 | { 391 | "date": "2008-10-01", 392 | "text": "Creatures in the same band must all attack the same player or planeswalker." 393 | }, 394 | { 395 | "date": "2009-10-01", 396 | "text": "If a creature in combat has banding, its controller assigns damage for creatures blocking or blocked by it. That player can ignore the damage assignment order when making this assignment." 397 | } 398 | ], 399 | "printings": [ 400 | "DRK", 401 | "4ED", 402 | "5ED" 403 | ], 404 | "originalText": "Banding, first strike", 405 | "originalType": "Summon — Pikemen", 406 | "legalities": [ 407 | { 408 | "format": "Commander", 409 | "legality": "Legal" 410 | }, 411 | { 412 | "format": "Legacy", 413 | "legality": "Legal" 414 | }, 415 | { 416 | "format": "Vintage", 417 | "legality": "Legal" 418 | } 419 | ], 420 | "id": "3aae00072a81589de279963e7c7def30ed1e0085" 421 | }, 422 | { 423 | "name": "Preacher", 424 | "manaCost": "{1}{W}{W}", 425 | "cmc": 3, 426 | "colors": [ 427 | "White" 428 | ], 429 | "colorIdentity": [ 430 | "W" 431 | ], 432 | "type": "Creature — Human Cleric", 433 | "types": [ 434 | "Creature" 435 | ], 436 | "subtypes": [ 437 | "Human", 438 | "Cleric" 439 | ], 440 | "rarity": "Rare", 441 | "set": "DRK", 442 | "setName": "The Dark", 443 | "text": "You may choose not to untap Preacher during your untap step.\n{T}: For as long as Preacher remains tapped, gain control of target creature of an opponent's choice he or she controls.", 444 | "artist": "Quinton Hoover", 445 | "power": "1", 446 | "toughness": "1", 447 | "layout": "normal", 448 | "multiverseid": 1816, 449 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1816&type=card", 450 | "reserved": true, 451 | "rulings": [ 452 | { 453 | "date": "2004-10-04", 454 | "text": "The target of the ability is a creature the chosen opponent controls. As the ability resolves, if the target isn't a creature controlled by the chosen opponent, the ability will be countered. If the ability's target is changed somehow, the new target must be a creature controlled by the chosen opponent." 455 | }, 456 | { 457 | "date": "2007-09-16", 458 | "text": "Whether Preacher is tapped is checked continually, starting when the ability is activated. If, before the ability resolves, there's any point at which Preacher is untapped, the ability has no effect — even if Preacher is tapped again by the time the ability resolves." 459 | }, 460 | { 461 | "date": "2007-09-16", 462 | "text": "When you activate the ability, you choose an opponent, then that opponent chooses the target." 463 | } 464 | ], 465 | "printings": [ 466 | "DRK", 467 | "MED" 468 | ], 469 | "originalText": "{T}: Gain control of one of opponent's creatures. Opponent chooses which target creature you control. If Preacher becomes untapped, you lose control of this creature; you may choose not to untap Preacher as normal during your untap phase. You also lose control of the creature if Preacher leaves play or at end of game.", 470 | "originalType": "Summon — Preacher", 471 | "legalities": [ 472 | { 473 | "format": "Commander", 474 | "legality": "Legal" 475 | }, 476 | { 477 | "format": "Legacy", 478 | "legality": "Legal" 479 | }, 480 | { 481 | "format": "Vintage", 482 | "legality": "Legal" 483 | } 484 | ], 485 | "id": "3764452e83809ac49340c57fcda8a0a1db8718ff" 486 | }, 487 | { 488 | "name": "Squire", 489 | "manaCost": "{1}{W}", 490 | "cmc": 2, 491 | "colors": [ 492 | "White" 493 | ], 494 | "colorIdentity": [ 495 | "W" 496 | ], 497 | "type": "Creature — Human Soldier", 498 | "types": [ 499 | "Creature" 500 | ], 501 | "subtypes": [ 502 | "Human", 503 | "Soldier" 504 | ], 505 | "rarity": "Common", 506 | "set": "DRK", 507 | "setName": "The Dark", 508 | "flavor": "\"Of twenty yeer of age he was, I gesse. Of his stature he was of even lengthe. And wonderly deliver, and greete of strengthe.\" —Geoffrey Chaucer, The Canterbury Tales", 509 | "artist": "Dennis Detwiller", 510 | "power": "1", 511 | "toughness": "2", 512 | "layout": "normal", 513 | "multiverseid": 1817, 514 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1817&type=card", 515 | "printings": [ 516 | "DRK", 517 | "TSB" 518 | ], 519 | "originalType": "Summon — Squire", 520 | "legalities": [ 521 | { 522 | "format": "Commander", 523 | "legality": "Legal" 524 | }, 525 | { 526 | "format": "Legacy", 527 | "legality": "Legal" 528 | }, 529 | { 530 | "format": "Modern", 531 | "legality": "Legal" 532 | }, 533 | { 534 | "format": "Time Spiral Block", 535 | "legality": "Legal" 536 | }, 537 | { 538 | "format": "Vintage", 539 | "legality": "Legal" 540 | } 541 | ], 542 | "id": "84bb772acb41c274dea3419fbac3e4ff66d67dd0" 543 | }, 544 | { 545 | "name": "Tivadar's Crusade", 546 | "manaCost": "{1}{W}{W}", 547 | "cmc": 3, 548 | "colors": [ 549 | "White" 550 | ], 551 | "colorIdentity": [ 552 | "W" 553 | ], 554 | "type": "Sorcery", 555 | "types": [ 556 | "Sorcery" 557 | ], 558 | "rarity": "Uncommon", 559 | "set": "DRK", 560 | "setName": "The Dark", 561 | "text": "Destroy all Goblins.", 562 | "artist": "Dennis Detwiller", 563 | "layout": "normal", 564 | "multiverseid": 1818, 565 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1818&type=card", 566 | "printings": [ 567 | "DRK", 568 | "MED" 569 | ], 570 | "originalText": "All Goblins are destroyed.", 571 | "originalType": "Sorcery", 572 | "legalities": [ 573 | { 574 | "format": "Commander", 575 | "legality": "Legal" 576 | }, 577 | { 578 | "format": "Legacy", 579 | "legality": "Legal" 580 | }, 581 | { 582 | "format": "Vintage", 583 | "legality": "Legal" 584 | } 585 | ], 586 | "id": "590343aada75b2a5108bdd166755005034c94a67" 587 | }, 588 | { 589 | "name": "Witch Hunter", 590 | "manaCost": "{2}{W}{W}", 591 | "cmc": 4, 592 | "colors": [ 593 | "White" 594 | ], 595 | "colorIdentity": [ 596 | "W" 597 | ], 598 | "type": "Creature — Human Cleric", 599 | "types": [ 600 | "Creature" 601 | ], 602 | "subtypes": [ 603 | "Human", 604 | "Cleric" 605 | ], 606 | "rarity": "Rare", 607 | "set": "DRK", 608 | "setName": "The Dark", 609 | "text": "{T}: Witch Hunter deals 1 damage to target player.\n{1}{W}{W}, {T}: Return target creature an opponent controls to its owner's hand.", 610 | "artist": "Jesper Myrfors", 611 | "power": "1", 612 | "toughness": "1", 613 | "layout": "normal", 614 | "multiverseid": 1819, 615 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1819&type=card", 616 | "printings": [ 617 | "DRK", 618 | "CHR", 619 | "TSB" 620 | ], 621 | "originalText": "{T}: Witch Hunter does 1 damage to target player.\n{1}{W}{W}, {T}: Return target creature opponent controls from play to owner's hand. Enchantments on target creature are destroyed.", 622 | "originalType": "Summon — Hunter", 623 | "legalities": [ 624 | { 625 | "format": "Commander", 626 | "legality": "Legal" 627 | }, 628 | { 629 | "format": "Legacy", 630 | "legality": "Legal" 631 | }, 632 | { 633 | "format": "Modern", 634 | "legality": "Legal" 635 | }, 636 | { 637 | "format": "Time Spiral Block", 638 | "legality": "Legal" 639 | }, 640 | { 641 | "format": "Vintage", 642 | "legality": "Legal" 643 | } 644 | ], 645 | "id": "ab8695981f477dc78e7bb37f8f31e1f5887f190a" 646 | }, 647 | { 648 | "name": "Dark Heart of the Wood", 649 | "manaCost": "{B}{G}", 650 | "cmc": 2, 651 | "colors": [ 652 | "Black", 653 | "Green" 654 | ], 655 | "colorIdentity": [ 656 | "B", 657 | "G" 658 | ], 659 | "type": "Enchantment", 660 | "types": [ 661 | "Enchantment" 662 | ], 663 | "rarity": "Common", 664 | "set": "DRK", 665 | "setName": "The Dark", 666 | "text": "Sacrifice a Forest: You gain 3 life.", 667 | "flavor": "Even the Goblins shun this haunted place, where the tree limbs twist in agony and the ground seems to scuttle under your feet.", 668 | "artist": "Christopher Rush", 669 | "layout": "normal", 670 | "multiverseid": 1820, 671 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1820&type=card", 672 | "printings": [ 673 | "DRK", 674 | "RAV" 675 | ], 676 | "originalText": "You may sacrifice a forest to gain 3 life.\nCounts as both a black card and a green card.", 677 | "originalType": "Enchantment", 678 | "legalities": [ 679 | { 680 | "format": "Commander", 681 | "legality": "Legal" 682 | }, 683 | { 684 | "format": "Legacy", 685 | "legality": "Legal" 686 | }, 687 | { 688 | "format": "Modern", 689 | "legality": "Legal" 690 | }, 691 | { 692 | "format": "Ravnica Block", 693 | "legality": "Legal" 694 | }, 695 | { 696 | "format": "Vintage", 697 | "legality": "Legal" 698 | } 699 | ], 700 | "id": "88c5783840af456fbfdd51d05074d7fd7f60f673" 701 | }, 702 | { 703 | "name": "Marsh Goblins", 704 | "manaCost": "{B}{R}", 705 | "cmc": 2, 706 | "colors": [ 707 | "Black", 708 | "Red" 709 | ], 710 | "colorIdentity": [ 711 | "B", 712 | "R" 713 | ], 714 | "type": "Creature — Goblin", 715 | "types": [ 716 | "Creature" 717 | ], 718 | "subtypes": [ 719 | "Goblin" 720 | ], 721 | "rarity": "Common", 722 | "set": "DRK", 723 | "setName": "The Dark", 724 | "text": "Swampwalk (This creature can't be blocked as long as defending player controls a Swamp.)", 725 | "flavor": "Even the other Goblin races shun the Marsh Goblins, thanks to certain unwholesome customs they practice.", 726 | "artist": "Quinton Hoover", 727 | "power": "1", 728 | "toughness": "1", 729 | "layout": "normal", 730 | "multiverseid": 1821, 731 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1821&type=card", 732 | "printings": [ 733 | "DRK" 734 | ], 735 | "originalText": "Swampwalk\nCounts as both a black card and a red card.", 736 | "originalType": "Summon — Goblins", 737 | "legalities": [ 738 | { 739 | "format": "Commander", 740 | "legality": "Legal" 741 | }, 742 | { 743 | "format": "Legacy", 744 | "legality": "Legal" 745 | }, 746 | { 747 | "format": "Vintage", 748 | "legality": "Legal" 749 | } 750 | ], 751 | "id": "d283f1ca077f7a4eb29df8a9aef99dcee3e21163" 752 | }, 753 | { 754 | "name": "Scarwood Goblins", 755 | "manaCost": "{R}{G}", 756 | "cmc": 2, 757 | "colors": [ 758 | "Red", 759 | "Green" 760 | ], 761 | "colorIdentity": [ 762 | "R", 763 | "G" 764 | ], 765 | "type": "Creature — Goblin", 766 | "types": [ 767 | "Creature" 768 | ], 769 | "subtypes": [ 770 | "Goblin" 771 | ], 772 | "rarity": "Common", 773 | "set": "DRK", 774 | "setName": "The Dark", 775 | "flavor": "Larger and more cunning than most Goblins, Scarwood Goblins are thankfully found only in isolated pockets.", 776 | "artist": "Ron Spencer", 777 | "power": "2", 778 | "toughness": "2", 779 | "layout": "normal", 780 | "multiverseid": 1822, 781 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1822&type=card", 782 | "printings": [ 783 | "DRK" 784 | ], 785 | "originalText": "Counts as both a green card and a red card.", 786 | "originalType": "Summon — Goblins", 787 | "legalities": [ 788 | { 789 | "format": "Commander", 790 | "legality": "Legal" 791 | }, 792 | { 793 | "format": "Legacy", 794 | "legality": "Legal" 795 | }, 796 | { 797 | "format": "Vintage", 798 | "legality": "Legal" 799 | } 800 | ], 801 | "id": "3ac2c2521d3e57eae45969d47297046ecbbc37cf" 802 | }, 803 | { 804 | "name": "City of Shadows", 805 | "cmc": 0, 806 | "type": "Land", 807 | "types": [ 808 | "Land" 809 | ], 810 | "rarity": "Rare", 811 | "set": "DRK", 812 | "setName": "The Dark", 813 | "text": "{T}, Exile a creature you control: Put a storage counter on City of Shadows.\n{T}: Add {C} to your mana pool for each storage counter on City of Shadows.", 814 | "artist": "Tom Wänerstrand", 815 | "layout": "normal", 816 | "multiverseid": 1823, 817 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1823&type=card", 818 | "reserved": true, 819 | "rulings": [ 820 | { 821 | "date": "2004-10-04", 822 | "text": "The storage counters aren't removed when you activate the mana ability." 823 | }, 824 | { 825 | "date": "2009-10-01", 826 | "text": "You can activate City of Shadows's second ability while it has no storage counters on it, though it won't add any mana to your mana pool in this case. You did not tap it for mana, though, in case any abilities (such as the ability of a Fertile Ground enchanting it) care about that." 827 | } 828 | ], 829 | "printings": [ 830 | "DRK", 831 | "ME3" 832 | ], 833 | "originalText": "{T}: Sacrifice one of your creatures, but remove it from the game instead of placing it in your graveyard. Put a counter on City of Shadows. \n{T}: Add X colorless mana to your mana pool, where X is the number of counters on City of Shadows.", 834 | "originalType": "Land", 835 | "legalities": [ 836 | { 837 | "format": "Commander", 838 | "legality": "Legal" 839 | }, 840 | { 841 | "format": "Legacy", 842 | "legality": "Legal" 843 | }, 844 | { 845 | "format": "Vintage", 846 | "legality": "Legal" 847 | } 848 | ], 849 | "id": "0dea34ee48ef8fcd9ab62fde8c282b12ecae90a4" 850 | }, 851 | { 852 | "name": "Maze of Ith", 853 | "cmc": 0, 854 | "type": "Land", 855 | "types": [ 856 | "Land" 857 | ], 858 | "rarity": "Uncommon", 859 | "set": "DRK", 860 | "setName": "The Dark", 861 | "text": "{T}: Untap target attacking creature. Prevent all combat damage that would be dealt to and dealt by that creature this turn.", 862 | "artist": "Anson Maddocks", 863 | "layout": "normal", 864 | "multiverseid": 1824, 865 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1824&type=card", 866 | "rulings": [ 867 | { 868 | "date": "2016-06-08", 869 | "text": "Maze of Ith can target an untapped attacking creature. It will still prevent damage in that case." 870 | }, 871 | { 872 | "date": "2016-06-08", 873 | "text": "The creature isn't removed from combat; it just has its damage prevented. It's still an attacking creature until the combat phase is complete." 874 | }, 875 | { 876 | "date": "2016-06-08", 877 | "text": "You can activate Maze of Ith's ability targeting an attacking creature you control during the combat damage step or the end of combat step. It'll be untapped and the damage it had already dealt won't be undone." 878 | } 879 | ], 880 | "printings": [ 881 | "DRK", 882 | "pJGP", 883 | "ME4", 884 | "V12", 885 | "EMA" 886 | ], 887 | "originalText": "{T}: Target attacking creature becomes untapped. This creature neither deals nor receives damage as a result of combat.", 888 | "originalType": "Land", 889 | "legalities": [ 890 | { 891 | "format": "Commander", 892 | "legality": "Legal" 893 | }, 894 | { 895 | "format": "Legacy", 896 | "legality": "Legal" 897 | }, 898 | { 899 | "format": "Vintage", 900 | "legality": "Legal" 901 | } 902 | ], 903 | "id": "fc8a1bd1b65a96adf59a8bc8c5472c7b077cab70" 904 | }, 905 | { 906 | "name": "Safe Haven", 907 | "cmc": 0, 908 | "type": "Land", 909 | "types": [ 910 | "Land" 911 | ], 912 | "rarity": "Rare", 913 | "set": "DRK", 914 | "setName": "The Dark", 915 | "text": "{2}, {T}: Exile target creature you control.\nAt the beginning of your upkeep, you may sacrifice Safe Haven. If you do, return each card exiled with Safe Haven to the battlefield under its owner's control.", 916 | "artist": "Christopher Rush", 917 | "layout": "normal", 918 | "multiverseid": 1825, 919 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1825&type=card", 920 | "rulings": [ 921 | { 922 | "date": "2004-10-04", 923 | "text": "When creatures return to the battlefield, they are put onto the battlefield as if just cast. Creatures with X in the mana cost are treated as X is zero. Creatures which can pay costs when put onto the battlefield to determine abilities must have those costs paid at this time." 924 | }, 925 | { 926 | "date": "2004-10-04", 927 | "text": "Token creatures cease to exist when they leave the battlefield, so this effect just exiles them with no chance to bring them back like you can with cards." 928 | }, 929 | { 930 | "date": "2004-10-04", 931 | "text": "If changed to another land type, the creature cards are not lost but can't be released until the land reverts to normal." 932 | }, 933 | { 934 | "date": "2004-10-04", 935 | "text": "When the creature leaves the battlefield any damage or \"will be destroyed at some future time\" effects are removed from the creature." 936 | }, 937 | { 938 | "date": "2004-10-04", 939 | "text": "Auras on creatures are put into the graveyard and counters on creatures are removed when the creatures are sent to the Haven." 940 | }, 941 | { 942 | "date": "2004-10-04", 943 | "text": "Creatures return to the control of their owners, regardless of who controls the Haven when it is sacrificed." 944 | }, 945 | { 946 | "date": "2004-10-04", 947 | "text": "All cards in the Haven stay there even if they cease to be creatures. When the Haven is sacrificed, the cards come back onto the battlefield whether or not they are creatures." 948 | }, 949 | { 950 | "date": "2004-10-04", 951 | "text": "Creatures return to the battlefield simultaneously." 952 | } 953 | ], 954 | "printings": [ 955 | "DRK", 956 | "CHR", 957 | "TSB" 958 | ], 959 | "originalText": "{2}, {T}: Remove target creature you control from game. This ability is played as an interrupt.\nDuring your upkeep, sacrifice Safe Haven to return all creatures it has removed from game directly into play. Treat this as if they were just summoned.", 960 | "originalType": "Land", 961 | "legalities": [ 962 | { 963 | "format": "Commander", 964 | "legality": "Legal" 965 | }, 966 | { 967 | "format": "Legacy", 968 | "legality": "Legal" 969 | }, 970 | { 971 | "format": "Modern", 972 | "legality": "Legal" 973 | }, 974 | { 975 | "format": "Time Spiral Block", 976 | "legality": "Legal" 977 | }, 978 | { 979 | "format": "Vintage", 980 | "legality": "Legal" 981 | } 982 | ], 983 | "id": "6110906e97b8ecefc85fe3c42fba48a55212ef33" 984 | }, 985 | { 986 | "name": "Sorrow's Path", 987 | "cmc": 0, 988 | "type": "Land", 989 | "types": [ 990 | "Land" 991 | ], 992 | "rarity": "Rare", 993 | "set": "DRK", 994 | "setName": "The Dark", 995 | "text": "{T}: Choose two target blocking creatures an opponent controls. If each of those creatures could block all creatures that the other is blocking, remove both of them from combat. Each one then blocks all creatures the other was blocking.\nWhenever Sorrow's Path becomes tapped, it deals 2 damage to you and each creature you control.", 996 | "artist": "Randy Asplund-Faith", 997 | "layout": "normal", 998 | "multiverseid": 1826, 999 | "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=1826&type=card", 1000 | "reserved": true, 1001 | "rulings": [ 1002 | { 1003 | "date": "2009-02-01", 1004 | "text": "This has two abilities. The second ability triggers any time it becomes tapped, whether to pay for its ability or not." 1005 | }, 1006 | { 1007 | "date": "2009-10-01", 1008 | "text": "The first ability can target any two blocking creatures a single opponent controls. Whether those creatures could block all creatures the other is blocking isn't determined until the ability resolves." 1009 | }, 1010 | { 1011 | "date": "2009-10-01", 1012 | "text": "A \"blocking creature\" is one that has been declared as a blocker this combat, or one that was put onto the battlefield blocking this combat. Unless that creature leaves combat, it continues to be a blocking creature through the end of combat step, even if the creature or creatures that it was blocking are no longer on the battlefield or have otherwise left combat by then." 1013 | }, 1014 | { 1015 | "date": "2009-10-01", 1016 | "text": "When Sorrow's Path's first ability is activated, its second ability triggers and goes on the stack on top of the first ability. The second ability resolves first, and may cause some of the attacking creatures to be dealt lethal damage." 1017 | }, 1018 | { 1019 | "date": "2009-10-01", 1020 | "text": "When determining whether a creature could block all creatures the other is blocking, take into account evasion abilities (like flying), protection abilities, and other blocking restrictions, as well as abilities that allow a creature to block multiple creatures or block as though a certain condition were true. Take into account whether those creatures are tapped, but not whether they have costs to block (since those apply only as blockers are declared)." 1021 | }, 1022 | { 1023 | "date": "2009-10-01", 1024 | "text": "When the first ability resolves, if all the creatures that one of the targeted creatures was blocking have left combat, then the other targeted creature is considered to be able to block all creatures the first creature is blocking. If the ability has its full effect, the second creature will be removed from combat but not returned to combat; it doesn't block anything." 1025 | }, 1026 | { 1027 | "date": "2009-10-01", 1028 | "text": "Abilities that trigger whenever one of the targeted creatures blocks will trigger when the first ability resolves, because those creatures will change from not blocking (since they're removed from combat) to blocking. It doesn't matter if those abilities triggered when those creatures blocked the first time. Abilities that trigger whenever one of the attacking creatures becomes blocked will not trigger again, because they never stopped being blocked creatures. Abilities that trigger whenever a creature blocks one of the attacking creatures will trigger again, though; those kinds of abilities trigger once for each creature that blocks." 1029 | } 1030 | ], 1031 | "printings": [ 1032 | "DRK", 1033 | "ME3" 1034 | ], 1035 | "originalText": "{T}: Exchange two of opponent's blocking creatures. This exchange may not cause an illegal block. Sorrow's Path does 2 damage to you and 2 damage to each creature you control whenever it is tapped.", 1036 | "originalType": "Land", 1037 | "legalities": [ 1038 | { 1039 | "format": "Commander", 1040 | "legality": "Legal" 1041 | }, 1042 | { 1043 | "format": "Legacy", 1044 | "legality": "Legal" 1045 | }, 1046 | { 1047 | "format": "Vintage", 1048 | "legality": "Legal" 1049 | } 1050 | ], 1051 | "id": "0206139ea48314cc60c714ffe9aef2d531f187cd" 1052 | } 1053 | ] 1054 | } --------------------------------------------------------------------------------