So, you want add your own cards + play shipfic with them online?

Excellent, let's get down into how to do it.

Step 0: Have art for your shipfic cards

It goes without saying that if you want to add your own cards, you first need to have cards!

Check out the TSSSF card generator as well as the released card assets from Horrible People. TSSSF.net uses 788x1088 .png images for all card art

Once you have your .png files ready, you'll need to upload them online. If you're only making a small number of shipfic cards, uploading them to a 3rd party website like the secret shipfic booru or derpibooru is perfect. If you plan on making dozens of cards and releasing multiple expansion packs, you chould check out Appendix A: Pack Format for a better way to organizing your packs

Step 1: Create a pack.json file

All the metadata for each shipfic card is stored in a JSON file. JSON is a very common file format which you can edit easily in a free text editor like notepad++.

The pack.json file doesn't need to be named pack.json, but it does need to have the following structure:

{
	"name": "My Shipfic Cards",
	"format": "link:1",
	"namespace": "authorName.packName",
	
	"cards": {
		"pony": {
			// add any pony cards here
		},

		"ship": {
			// add any ship cards here
		},

		"goal": {
			// add any goal cards here
		},

		"start": {
			// add any start cards here
		}
	}
}

Here, the name attribute is what you want to display as the name of your pack, and the namespace attribute is a unique name associated with your pack (it's standard to do authorName.packName, but you can use anything as long as it only uses A-z characters and periods)

Replace the // add comments with additional JSON to add your cards to the pack, like so. Make sure to replace the names with ones that fit and the URLS with URLs that point to your uploaded cards

{
	"name": "My Shipfic Cards",
	"format": "link:1",
	"namespace": "authorName.packName",
	
	"cards": {
		"pony": {
			"BrokenWingRainbowDash": {
				"url": "https://tsssf.net/packs/Core/Pony/BrokenWingRainbowDash.png"
			},
			"UnicornChangeling": {
				"url": "https://tsssf.net/packs/Core/Pony/UnicornChangeling.png"
			}
		},

		"ship": {
			"LovePoisonIsNoJoke": {
				"url": "https://tsssf.net/packs/Core/Ship/LovePoisonIsNoJoke.png"
			}
		},

		"goal": {
			"ItsNotEvil": {
				"url": "https://tsssf.net/packs/Core/Goal/ItsNotEvil.png"
			}
		},

		"start": {
			"FanficAuthorTwilight": {
				"url": "https://tsssf.net/packs/Core/Start/FanficAuthorTwilight.png"
			}
		}
	}
}

Step 2: Add Additional Attributes to Each Card

Now that every card has an image associated with it, it's time to add additional attributes specific to whether it's a pony card, a ship card, or a goal card.

There's many, many more attributes used by TSSSF.net across all the cards, but the above example should be enough to get you started. See Appendix B: Pony, Ship, + Goal attributes for a complete list, or check out the core deck's pack.jsonfile for even more examples.

{
	"name": "My Shipfic Cards",
	"format": "link:1",
	"namespace": "authorName.packName",
	
	"cards": {
		"pony": {
			"BrokenWingRainbowDash": {
				"url": "https://tsssf.net/packs/Core/Pony/BrokenWingRainbowDash.png",
				"name": "Rainbow Dash",
				"race": "pegasus",
				"gender": "female",
				"action": "replace",
				"keywords": [
					"Mane 6"
				]
			},
			"UnicornChangeling": {
				"url": "https://tsssf.net/packs/Core/Pony/UnicornChangeling.png",
				"name": "Changeling",
				"race": "unicorn",
				"action": "Changeling(unicorn)",
				"keywords": [
					"Changeling",
					"Villain"
				]
			}
		},

		"ship": {
			"LovePoisonIsNoJoke": {
				"url": "https://tsssf.net/packs/Core/Ship/LovePoisonIsNoJoke.png",
				"action": "lovePoison"
			}
		},

		"goal": {
			"ItsNotEvil": {
				"url": "https://tsssf.net/packs/Core/Goal/ItsNotEvil.png",
				"points": "1"
			}
		},

		"start": {
			"FanficAuthorTwilight": {
				"url": "https://tsssf.net/packs/Core/Start/FanficAuthorTwilight.png",
				"name": "Twilight Sparkle",
				"race": "unicorn",
				"gender": "female",
				"keywords": [
					"Mane 6"
				]
			}
		}
	}
}

Step 3: Upload to TSSSF.net

Now that you have your json file ready to go, it's time to upload it! Host a new game, open the 'Choose Cards' tab, and upload your JSON file.

If you've done everything correctly, you should now see your cards on the page and you can now play shipfic with them!

Appendix A: Pack Format

The link:1 format, described above, is a good way to quickly put together a pack of cards to play online. However, for larger collections of cards it is convenient to store all the cards in the same folder structure, rather than linking individual cards together. Hence, the pack:1 format.

The Pack:1 File Structure

The difference between the pack:1 format and the link:1 format is that the pack:1 format omits the url attributes and instead, stores the images files in a consistent location based on the card's key name, card type, namespace, and a root attribute, e.g.

root/namespace/cardType/cardKey.png

For example, the following pack.json file

{
	"name": "My Shipfic Cards",
	"format": "pack:1",
	"namespace": "authorName.packName",
	"root": "https://tsssf.net/packs",
	
	"cards": {
		"pony": {
			"BrokenWingRainbowDash": {
				"name": "Rainbow Dash",
				"race": "pegasus",
				"gender": "female",
				"action": "replace",
				"keywords": [
					"Mane 6"
				]
			}
		},

		"ship": {
			"LovePoisonIsNoJoke": {
				"action": "lovePoison"
			}
		},

		"goal": {
			"ItsNotEvil": {
				"points": "1"
			}
		},

		"start": {
			"FanficAuthorTwilight": {
				"name": "Twilight Sparkle",
				"race": "unicorn",
				"gender": "female",
				"keywords": [
					"Mane 6"
				]
			}
		}
	}
}

corresponds to image files uploaded to the following paths:

https://tsssf.net/packs/authorName/packName/Pony/BrokenWingRainbowDash.png
https://tsssf.net/packs/authorName/packName/Ship/LovePoisonIsNoJoke.png
https://tsssf.net/packs/authorName/packName/Goal/ItsNotEvil.png
https://tsssf.net/packs/authorName/packName/Start/FanficAuthorTwilight.png

Note that periods in the namespace attribute correspond to nested folders in the path name.

Thumbnail Images

In addition to the above file structure, every .png image has a corresponding thumbnail image, suffixed with .thumb.jpg.

https://tsssf.net/packs/authorName/packName/Pony/BrokenWingRainbowDash.png
https://tsssf.net/packs/authorName/packName/Pony/BrokenWingRainbowDash.thumb.jpg

The .thumb.jpg images are only 197 x 272 pixels and are much smaller than their .png counterparts, making them load much more quickly.

Appendix B: Pony, Ship, + Goal Attributes

For examples of cards which use the attributes explained in this section, see the various pack.json files used by tsssf.net.

Core/pack.json
EC/pack.json
PU/pack.json
NoHoldsBarred/pack.json

Pony Attributes

AttributeValid ValuesNotes/Description
nameAny string, required
"name1/name2/..."
Should be the simplest name of the pony in question, e.g. Twilight Sparkle for cards like Fanfic Author Twilight
race"earth"
"pegasus"
"unicorn"
"alicorn"
"race1/race2/..."Any combination of the above races separated by a slash. Will count as each of those races.
gender"male"
"female"
"male/female"Counts as both male and female for all goals
keywordsAny string arrayContains all non-name keywords for the pony
altTimelinetrueGives a card the altTimeline/apocalypse/hour glass symbol
changeGoalPointValuestrueWhen this card is on the grid, prevents goal cards from being worth their normal amount of points
countany numberChanges how many ponies this card counts as, e.g. 2 for Aloe & Lotus
action"swap"
"3swap"
"replace"
"search"
"copy"
"fullCopy"Copies a card's power as well as all its symbols, names, keywords, etc.
"draw"
"newGoal"
"playFromDiscard"
"interrupt"Allows the card to a pony card which was just played, even if it's not that player's turn.
"ship"Allows the pony card to be played as a ship as well
"shipWithEverypony"Special ability for HorriblePeople.GraciousGivers.Pony.PrincessCelestAI
"Changeling(<type>)"Allows the card to set a disguise when played and when moved. <type> can be any of the following:
earth
pegasus
unicorn
alicorn
nonAlicornFemale
plushling
replace
"ChangelingNoRedisguise(<type>)" Same as Changeling(<type>), but the card cannot redisguise when moved
"exchangeCardsBetweenHands"Causes players to randomly trade one card with the player next to them

Ship Attributes

AttributeValid ValuesNotes/Description
action"genderChange"
"raceChange"
"timelineChange"
"lovePoison"
"makePrincess"
"keywordChange"
"clone"
"addKeywords(<list>)" <list> is a list of keywords (no quotation marks) separated by commas, e.g.
Object
Nightmare, Villain
"raceGenderChange"
"keywordChange"

Goal Attributes

AttributeValid ValuesNotes/Description
pointsany number in quotes, e.g. "3"
two integers (second one larger than the first), separated by a hyphen in quotes
e.g."3-4"
Used for cards which can be worth different point values, depending on certain criteria
goalLogicSee Appendix C: Goal Logic

Appendix C: Goal Logic

Goal logic is an attribute on goal cards which specifies the criteria needed to achieve a goal. Because there are many different kinds of goals and criteria, goal logic is expressed in its own language and has its own specific syntax. To give you an idea to what the language looks like, here are a few examples of the text written on various goal cards and the goal logic associated with it.

Card TextGoal Logic
Win this goal when 6 earth pony/earth pony ships are on the gridExistsShip(race=earth,race=earth,6)
Win this goal when at least 5 time travelers are on the gridExistsPony(altTimeline=true, 5)
Win this Goal when Rainbow Dash is shipped with any 3 femalesExistsPonyShippedTo(name=Rainbow Dash, Select(gender=female,3))
Win this goal when 6 ponies with the Mane 6 keyword are shipped in a chainExistsChain(Mane 6 in keywords, 6)
Win this goal when you break up Shining Armor with any female except Twilight SparkleBreakShip(name=Shining Armor, gender=female && name != Twilight Sparkle)

Goal functions

Valid goal logic consists of a single goal function with all its criteria/count parameters passed in to it. Each goal function checks something different, so make sure you use the right one for your goal

ExistsPony
Syntax: 
ExistsPony(<criterion>)
ExistsPony(<criterion>, <count>)
Use: 
Checks for whether a pony matching criterion is on the grid. If count is provided, checks that there are <count> distinct ponies which match the criterion
ExistsShip
Syntax: 
ExistsShip(<criterion1>, <criterion2>)
ExistsShip(<criterion1>, <criterion2>, <count>)
Use: 
Checks for whether a pony matching criterion1 is shipped with a pony matching criterion2. If count is provided, checks that there are <count> distinct ships which match the criteria
ExistsPonyShippedTo
Syntax: 
ExistsPonyShippedTo(<criterion1>, Select(<criterion2>, <count>))
Use: 
Checks for whether a pony matching criterion1 is shipped with count other ponies, each of which match criterion2
Syntax: 
ExistsPonyShippedTo(<criterion1>, AllOf(<criterion2>, <criterion3>, ...))
Use: 
Checks for whether a pony matching criterion1 is shipped with several ponies, each of which matches a different criterion specified in AllOf. AllOf takes in any number of criterion parameters, each separated by a comma
Syntax: 
ExistsPonyShippedTo(<criterion>, ShippedWith2Versions)
Use: 
Checks for whether a pony matching the criterion is shipped with two different versions of a pony
ExistsChain
Syntax: 
ExistsChain(<criterion>, <count>)
Use: 
Checks for whether a chain of size count exists on the grid, where each pony in the chain matches the specified criterion.
PlayPonies
Syntax: 
PlayPonies(<criterion>, <count>)
Use: 
Checks for whether a player has played count ponies this turn where the pony matches the specified criterion.
PlayShips
Syntax: 
PlayShips(<criterion1>, <criterion2>, <count>)
Use: 
Checks for whether a player has played count ships this turn where one of the two ponies matches criterion1 and the other matches criterion2
PlayShipCards
Syntax: 
PlayShipCards(<criterion>)
PlayShipCards(<criterion>, <count>)
Use: 
Checks for whether a player has played a certain number of ship cards (as opposed to ships) matching the criteria this turn.
BreakShip
Syntax: 
BreakShip(<criterion1>, <criterion2>)
BreakShip(<criterion1>, <criterion2>, <count>)
Use: 
Checks for whether a player has broken a ship this turn where one of the ponies matches criterion1 and the other criterion2. If count is provided, checks that such a ship has been broken count times this turn (it can be the same ship multiple times, or several different ships)
SwapCount
Syntax: 
SwapCount(<count>)
Use: 
Checks whether at least count different cards have been swapped this turn.

There's a few other niche goal functions, but they exist primarily for special cards

ExistsShipGeneric
Syntax: 
ExistsShipGeneric(ShippedWithOppositeGenderedSelf)
Use: 
Checks whether any ship exists between a pony and a gender-swapped version of the same pony.

Criteria

Criteria are what actually determine if a particular pony card matches or not based on the pony card's attributes. They are passed in as parameters to goal functions

<attribute> = <value>Matches when the card has attribute set to value
<attribute> != <value>Matches when the card does NOT have attribute set to value
<value> in keywordsMatches when the card has value in its keywords
<value> !in keywordsMatches when the card does NOT have value in its keywords
genderSwappedMatches cards which have had their gender swapped
*Matches every card

Criteria do not use quotation marks anywhere for simplicity's sake. Whitespace between operators like = and != is not necessary, though may help readability. For names which contain two or more words, the whitespace in the name will not be removed, but the whitespace before and after the name will. Names and attributes are cAsE sEnSiTiVe

You can combine multiple criteria into a single check with the following criteria

<criterion1> || <criterion2>Matches when the card matches criterion1, criterion2, or both
<criterion1> && <criterion2>Matches when the card matches both criterion1 and criterion2

During the game, cards can sometimes change genders, change races, or gain new keywords. Thus, it is sometimes helpful for criteria to test what the card's original attributes were, rather than what they are now. This can be accommodated by adding "_b" (b for base) to an attribute's name. Here's an example for a criterion which uses this to test if a card has been changed into an alicorn

race=alicorn && race_b != alicorn