Skip to content
davewx7 edited this page Aug 6, 2018 · 8 revisions

Argentum Age makes it easy to create new cards, using a flexible system which allows a broad range of cards to be created.

Getting Started

To create your own cards, get setup with access to Argentum Age's data folder. You can follow the GettingStartedAsADeveloper guide to learn how to do this.

Overview

Cards are defined in text files, found in the data/ folder. These files include,

  • cards-entropia.cfg -- the core set collection of Entropia cards.
  • cards-materia.cfg -- the core set collection of Materia cards.
  • cards-minerva.cfg -- the core set collection of Minerva cards.
  • cards-aether.cfg -- the core set collection of Aether cards.
  • cards-gaea.cfg -- the core set collection of Gaea cards.

To begin with you can open one of these files in a text editor and look around it to become familiar with it. Search for some of your favorite cards to see how they are defined.

Later, if you want to add your own file with cards rather than adding cards to an existing one, edit cards.cfg to add your new file to the list -- cards.cfg contains a master list of all files with card definitions.

A minimal creature card

Let's get started by defining a minimal creature card and seeing how we can test it in-game. In cards-materia.cfg add the following entry:

	"Example Warrior": {
		name: "Example Warrior",
		type: "creature",
		portrait: "mercenary.png",
                hue_shift: 0.5,
		cost: 3,
		school: "@eval MATERIA",
		creature: {
			attack: 5,
			life: 5,
		}
	},

If you save your file, start the game, start a map, you can press ctrl+d to open the debug console and type this command:

debug_card('Example Warrior')

You will draw an instance of the card into your hand. This is the best and easiest way to test a card you have created.

Let's step through the attributes and their meaning:

	"Example Warrior": {
		name: "Example Warrior",

The name must match with the key you gave above. This name is used both to identify the card in code and is the name that will be displayed to the player.

		type: "creature",

The type of the card. This is either "creature" or "spell". (Lands use "creature").

		portrait: "mercenary.png",
                hue_shift: 0.5,

This defines the portrait the card uses. The portrait must exist in the images/portrait/card-size folder. The image should be a RGBA 32-bit PNG with dimensions of 480x420.

The hue_shift parameter is an optional parameter that shifts the hue of the image. It isn't normally used for 'final' cards but is very useful if you want to add a new card and simply use an existing image from the game temporarily. Here we use the Mercenary's image but shift the hue so our new Example Warrior card looks distinct from the Mercenary card. Our eventual plan would be to add a example-warrior.png and remove the hue shift.

		cost: 3,
		school: "@eval MATERIA",

The mana cost and school of the card

		creature: {
			attack: 5,
			life: 5,
		}

The creature section is found only in cards with type "creature" and contain all the attributes that are specific to creatures. The bare minimum is attack and life, though there are plenty more possible attributes.

A minimal spell card

A spell card is trickier than a basic creature card because it has to do something when it is cast -- let you draw cards, let you kill a creature, let you move creatures. Something. A spell also needs to have targets, and different spells have different targets they consider valid.

As an example we are going to create a card which lets you teleport an enemy creature to any vacant location on the board.

	"Teleport Enemy": {
		name: "Teleport Enemy",
		school: "@eval MINERVA",
		type: "spell",
		portrait: "astral-walk.png",
		hue_shift: 0.5,

		cost: 2,
		is_response: true,

		rules: "Move target enemy creature to a target vacant location",

		possible_targets: "
		  def(class game_state game, class creature avatar, [Loc] targets) ->[Loc]|null
		  if(size(targets) = 0,
			 enemy_creatures_as_possible_targets(game, avatar, targets),
			 size(targets) = 1,
			 filter(game.all_locs, game.creature_at_loc(value) = null)
		  )
		",
		on_play: "def(class game_state game, class message.play_card info) ->commands
			game.creature_at_loc_or_die(info.targets[0]).set_loc(game, info.targets[1], 'blink')
		",
	},

Some of these attributes are familiar from the previous creature card we created. Let's look at the new attributes:

		is_response: true,

This makes the card a response. (Can be played during response phases).

		rules: "Move target enemy creature to a target vacant location",

This is the rules text that will appear on the card. Note that this isn't interpreted by the game at all. It is simply displayed verbatim on the card. It is your responsibility to make sure the rules text here matches what you make the spell do. Which is what we will look at next...

		possible_targets: "

This is a definition of the places the spell can target. Here we are going to have a formula which inspects the game state when the spell is cast and tells us which locations the player may target with the spell. We are about to dive into what the formula looks like, and it might seem a bit overwhelmingly complicated at first -- it is a powerful system and powerful systems are necessarily complex -- but don't worry, there are plenty of examples to guide you in the form of other cards.

		  def(class game_state game, class creature avatar, [Loc] targets) ->[Loc]|null

This defines the parameters the formula accepts and the result it gives back. possible_targets MUST define a formula in exactly this format. It accepts the 'game state' which it calls 'game'. The player casting the spell which it calls 'avatar'. A list of targets the player has selected so far, called 'targets'.

The formula must result in either a list of possible locations the player can choose from, or null if the current list of targets is acceptable and no more targets need adding.

		  if(size(targets) = 0,
			 enemy_creatures_as_possible_targets(game, avatar, targets),
			 size(targets) = 1,
			 filter(game.all_locs, game.creature_at_loc(value) = null)
		  )

This is the body of the formula -- the actual logic telling us which tiles the player can target with this spell.

Remember that 'targets' will contain the locations the player has targeted so far. When the player first clicks the card intending to cast it, 'targets' will be empty. This triggers the first condition:

		  if(size(targets) = 0,
			 enemy_creatures_as_possible_targets(game, avatar, targets),

enemy_creatures_as_possible_targets simply allows any enemy creature to be targeted. (As long as it doesn't have Cover or is otherwise not a valid target). This is exactly what we want.

Once we have targeted a creature we must choose an empty location for the spell. That's what the next part takes care of:

			 size(targets) = 1,
			 filter(game.all_locs, game.creature_at_loc(value) = null)

game.all_locs gives all possible locations on the board. Here we filter it by asking if there is a creature at the location. If there isn't then it's a place we can teleport to. So this will allow any tile with no creature in it for the second target.

This takes care of getting the targeting logic right. The game's UI will automatically constrain the player to only targets that this formula declares are legal.

Next we have the formula which actually controls what happens when the spell is cast. Interestingly this is actually much simpler than the possible_targets formula:

		on_play: "def(class game_state game, class message.play_card info) ->commands
			game.creature_at_loc_or_die(info.targets[0]).set_loc(game, info.targets[1], 'blink')
		",

Once again, on_play has a certain format it has to be in -- it accepts the game and then info which contains information about who cast the spell and most importantly what the targets are. It results in some commands which change the game in some way.

game.creature_at_loc_or_die(info.targets[0]) retrieves the creature that is at the first target location. Note the 'or_die' part -- it means "we are completely confident there WILL be a creature at the first location so make Argentum Age die with an error message if there isn't." We are confident because the possible_targets makes sure the first target is a creature.

.set_loc(game, info.targets[1], 'blink') sets the location of the creature to the second target. The 'blink' part specifies the type of animation the UI should use to display this movement. In this case we make it do a 'blink' animation. There are a variety of animations supported including 'move', 'leap', and 'swap'.

Creature abilities

Creatures in Argentum Age may have special abilities which give them special behavior. Creatures without special abilities are termed 'vanilla' creatures.

The easiest ability to add are the builtin abilities. To give a creature Cover and First Strike you would add this to their creature section:

abilities: ["First Strike", "Cover"]

Creatures may also have custom abilities. There are three types of abilities:

  • Triggered abilities: these are abilities that trigger an effect when some specific condition occurs. For instance, when the creature is summoned, when the creature dies, or when a spell is cast.
  • Activated abilities: these are similar to triggered abilities, but are triggered when the player chooses rather than when some condition occurs. Activated abilities are very similar to cards. The game considers an activated ability to effectively be a type of card.
  • Passive abilities: these are abilities that are constantly in effect for as long as the creature is alive.

This is an example of a Triggered ability taken from the Spell Warden:

			triggered_abilities: [{
				name: 'Spellbound',
				rules: "When you cast a spell, Spell Warden gets +1/+1",
				on_card_played: "def(class creature creature, class game_state game, class player player, class message.play_card info) ->commands
					if(info.player_index = creature.controller and info.card and info.card.type = 'spell', [
						add(creature.attack, 1),
						add(creature.life, 1),
					])
				",
			}],

There are plenty of examples of abilities in the card definitions and they are the best reference when adding a new ability of your own.

Card sets and the library

Above we showed how to use debug_card('Example Guard') to put a card in your hand during a game. Of course you want to know how to get a card into your collection, and have it appear in your library.

To do this you must place your card in a set. For instance:

set: "core",
rarity: 0,

This makes the card an uncommon card in the core set. (rarity 0 = common, 1 = uncommon, 2 = rare).

To purchase this card in your library the card must be known to the server. To have this happen you must either get your card accepted into the official servers, or you must run your own Argentum Age server and connect to it. Follow the directions in GettingStartedAsADeveloper under "Running your own local server" to do this.

Image adjustments

You can tweak how a card is displayed in-game -- its cropping and zooming -- by going to the titlescreen, accessing the debug console with ctrl+d and typing adjustcards.

Click on a card, and all the contexts in which that card is used are displayed. You can left-click + drag to adjust the panning and right-click + drag to adjust the zooming to frame the art perfectly.

Renaming Cards

Sometimes we want to rename a card. Early in development of a card this can be done easily. However, once we have distributed a card and players own it, we must always provide that card -- we don't want players to find their card disappearing on them.

we can, however, rename a card by using an alias. Suppose we have a card definition like this:

"Old Card Name": {
    name: "Old Card Name",
   ...rest of definition....
}

We can rename it, providing an alias, like this:

"Old Card Name": "New Card Name",
"New Card Name": {
    name: "New Card Name",
   ...rest of definition....
}

This will make the game understand that any references to "Old Card Name" should now map to "New Card Name".