This document is primarily for modders who want to incorporate their characters/items/etc into our mod.
If you're looking for information about characters/items/etc in this mod, visit the official Epiphany wiki.
Epiphany API Documentation
Updated 2nd of Feb '23
The mod can be accessed through the Epiphany
global.
While a large number of tables/functions in the mod can already be accessed from outside, we provide an API which contains functions that help with performing some common actions. The API can be accessed through Epiphany.API
.
A sample character that functions as a practical example on how to use the Epiphany API can be downloaded from here: https://gitlab.com/OpenSauce04/epiphany-api-sample
Adding a character
Adding an existing character to the tarnished character selection wheel is done through AddCharacter
function:
function Epiphany.API.AddCharacter(charInfo: table)
This function specifically should always be run under a MC_POST_NEW_LEVEL
callback. This is so that reguardless of load order, the character will be added after Epiphany is loaded, but before the character menu's first MC_POST_GAME_STARTED
callback. Within this function, the callback should be removed using RemoveCallback(CallbackID, Function)
, as it does not need to be run again.
charInfo
is a table that contains all required info about a character. It MUST contain the following fields:
charID
: integer - ID of your charactercharName
: string - Name of your character. Is used internally, does not have to be the same as xml character name.costume
: string - Path to the .anm2 file of your character.menuGraphics
: string - Path to the .anm2 file of your character's menu graphics.coopMenuSprite
: string - Path to the .anm2 file of your character's coop character selection menu icon.
Additionally, you may optionally include the following fields:
extraCostume
: integer array - IDs of additional costumes.hitSound
: SoundID - ID of the hurt sound of the character.deathSound
: SoundID - ID of the death sound of the character.unlockChecker
: function - Takes 0 arguments and returns whether the character is unlocked. Defaults to a function that always returns true.pocketItem
: integer - ID of your character's starting pocket item.pocketItemPersistent
: boolean - If set to true, pocket item set withpocketItem
field will be given back to the player if they somehow lose it.floorTutorial
: string - Path to a .anm2 file containing your floor tutorial text for your character relative to theresources
folder. There should be 2 animations in this file:Tutorial
should contain a single frame, that being the floor tutorial graphic. The center of the animation is the center of the room.BurningTutorial
should be identical toTutorial
, with the sole exception that the tint RGB values of the animation frame should be set to these values:
charStats
: table - Defines character's starting items/health/stat changes. May contain the following fields of number type (unless specified otherwise):DAMAGE_FLAT
- Flat damage bonusDAMAGE_ADD
- Damage bonus that follows vanilla damage formulaDAMAGE_MULTI
- Damage multiplierFIRERATE_MULTI
- Firerate multiplier, follows Repentance firerate formulaSHOTSPEED_ADD
- Shot speed bonusTEARHEIGHT
- Tear height reduction(?)SPEED_ADD
- Speed bonusLUCK
- Luck bonusFLYING
: boolean - If set to true, gives flightTEAR_FLAGS
: TearFlag - Character's innate tear flagsTEAR_COLOR
: Color - Tear colorLASER_COLOR
: Color - Laser color- Starting health:
HEALTH_RED
- Max red heartsHEALTH_SOUL
- Soul heartsHEALTH_BLACK
- Black heartsHEALTH_ROTTEN
- Rotten heartsHEALTH_BONE
- Bone heartsHEALTH_RED_EXTRA
- Additional red hearts (for filling bone hearts)HEALTH_ETERNAL
- Eternal heartsHEALTH_BROKEN
- Broken hearts
- Starting pickups:
COINS
BOMBS
KEYS
ITEMS
: integer array - Character's starting items
charInitFunction
: function - Code that is run when your custom character is initialized. This function recieves the player object as a parameter. An example of using this field would be:charInitFunction = function(player) print(player:GetPlayerType()) end
Example code
This is an example of adding a character through the API taken from the code of the Epiphany API sample character:
-- CHARACTER STATS
local stats_opensauce={
HEALTH_RED = 2,
HEALTH_ROTTEN = 1,
HEALTH_SOUL = 4
}
local OpenSauceID = Isaac.GetPlayerTypeByName("OpenSauce")
-- INITIALIZE EPIPHANY MENU CHARACTER
function Mod:AddOpenSauceCharacter()
-- DO NOT RUN IF THE API IS NOT LOADED
if not Epiphany or not Epiphany.API then return end
-- ADD THE ACTUAL CHARACTER
Epiphany.API.AddCharacter({
charName = "OPENSAUCE", --Internal character name (REQUIRED)
charID = OpenSauceID, -- Character ID (REQUIRED)
charStats = stats_opensauce, -- Stat array
costume = "gfx/characters/character_opensauce.anm2", -- Main costume (REQUIRED)
extraCostume = {Isaac.GetCostumeIdByPath("gfx/characters/character_opensauce_extra.anm2")}, -- Extra costume (e.g. Maggy's Hair)
menuGraphics = "gfx/ui/menu_opensauce.anm2", -- Character menu graphics (portrait, text) (REQUIRED)
coopMenuSprite = "gfx/ui/Coop/coop_menu_opensauce.anm2", -- Co-op menu icon (REQUIRED)
pocketItem = CollectibleType.COLLECTIBLE_SULFUR, -- Pocket active
pocketItemPersistent = true, -- Should the pocket active always be re-given when not present? (false is vanilla behaviour)
unlockChecker = function() return true end, -- function that returns whether the character is unlocked. Defaults to always returning true.
floorTutorial = "gfx/grid/tutorial_opensauce.anm2"
})
Mod:RemoveCallback(ModCallbacks.MC_POST_NEW_LEVEL, Mod.AddOpenSauceCharacter)
end
Mod:AddCallback(ModCallbacks.MC_POST_NEW_LEVEL, Mod.AddOpenSauceCharacter)
Adding Throwing Bag synergies
The Throwing Bag item was made with the ability to easily add item synergies in mind, so creating new synergies is relatively simple. Each bag synergy is defined as a subtable of Epiphany.Item.THROWING_BAG.SynergyTable
containing relevant information about that synergy. While it is possible to add synergies directly to that table, it's recommended to use the provided API function instead:
function Epiphany.API:AddCainBagSynergy(synergyName: string, synergyTable:table): table
synergyName
is the name of the bag synergy. It can be whatever as long as there aren't any conflicts between synergy names, but the convention is "itemname_<bagged|passive>"
.
synergyTable
is a table containing bag synergy info. It MUST contain following fields:
id_list
: integer array - Contains IDs of all items that contribute to that synergy.flags
: integer - Indicates when a synergy should be applied. Synergy flags can be accesssed throughEpiphany.Item.THROWING_BAG.SynergyFlags
, there are 2 flags:BAGGED
- Synergy applies when one of items fromid_list
is in a bag.PASSIVE
- Synergy applies when one of items fromid_list
is in player's inventory.
All other fields are optional, but needed for the bag to actually do anything. Those are:
color
: Color - Color of the bag.sprite_path
: string - Path to the .anm2 of the bag.- Callback functions:
- Common parameters explanation:
bag
: EntityEffect - Bag entitycount
: integer - Amount of items contributing to the synergyplayer
: EntityPlayer - Owner player of the bagentity
: Entity - The entity that got hit
callback_swing (bag, count, player)
- Called when the player starts swinging the bag.callback_swing_update(bag, count, player)
- Called every update while the bag is swinging.callback_swing_hit(bag, count, entity, player)
- Called when a swinging bag hits an enemy.callback_throw(bag, count)
- Called when the bag is thrown.callback_flying(bag, count)
- Called every frame while the bag is flying.callback_update(bag, count)
- Called every update while the bag is flying or on the ground, but not when swinging.callback_hit(bag, count, entity)
- Called when a flying bag hits an enemy.callback_unload(bag, count)
- Called when a bag fires tear burst.callback_fall(bag, count)
- Called when a bag falls.callback_remove(bag, count)
- Called when a flying bag is removed (usually by picking it up after it fell)
- Common parameters explanation:
- Burst params:
burst_color
: Color - Color of tears spawned withburst_params
burst_params
: table - Parameters used for when the bag scatters tears on hitcount_add_multi
: number - Minimum multiplier of tears that are spawned based on tearrate (default: 1.5)count_std_multi
: number - Maximum value for random addition to multiplier of tears (default: 0.1665)speed_add_multi
: number - Minimum multiplier of amount of speed of tears (default: 1, multiplied by 15)speed_std_multi
: number - Maximum value for random addition to multiplier of speed of tears (default: 0.2, multiplied by 15)damage_multi
: number - Damage multiplier of tears (default: 1)variant
: TearVariant - Variant of tears (default: TearVariant.BLUE)flags
:TearFlags -- TearFlags of tears (default: TEAR_NORMAL aka none)
parent
: string - Name of the synergy table entry to inherit properties from.- This parameter is mostly unused, but appears to define a secondary synergy to look into if the main synergy does not contain a field. For example, if parent of synergy A is synergy B, then if A does not contain
callback_swing
, bag code will look forcallback_swing
in synergy B.
- This parameter is mostly unused, but appears to define a secondary synergy to look into if the main synergy does not contain a field. For example, if parent of synergy A is synergy B, then if A does not contain
The function returns synergyTable
.
Bag data
Note: Swinging bag and thrown bag are two separate entities, when a bag is thrown the swinging bag is removed and a flying bag is spawned.
Throwing bags use a bunch of custom entity data variables. It's pointless to list all of them, but here are some that might be useful:
"BAG_ID"
: integer - Identifier of the bag that is unique per player. Present in both swinging and flying bag entities. Should be consistent between pickups/room changes/game restarts."BAG_DAMAGE_MULTIPLIER"
: number - Collision damage multiplier. Present in both swinging and flying bags. Defaults to 1.0, can be used to change bag's on hit damage"BAG_METADATA"
: table - Present in both swinging and flying bags."CONTENT"
: table - Dictionary with item ids in string form as keys and amount of that item in a bag as values."EXTRA_MASS"
: number - Bag mass appears to affect its collision properties, as well as damage. Defaults to 0
"SPAWNER_PLAYER"
: EntityPlayer - Bag's parent player. Present in both swinging and flying bags. For swinging bags, you may also useplayer
parameter supplied by swinging bag callbacks.BAG_FLAGS
: table - Present only in flying bags, a set that supports two flags:PIERCING
HOMING
Note: TR Cain's code is extremely convoluted and none of the people who wrote it are working on the mod anymore, so it's possible that some of the information above is incorrect or incomplete. We're going to attempt to untangle it for Wave 5
Object groups + Custom item pools
We split vanilla consumables, slot machines and some other things into thematic groups to have finer control over what types of consumables/slots/etc. can be spawned under specific conditions (for example, Turnover shops only spawn "demonic" beggars and hearts in devil rooms, some randomly rolled consumables can only be souls or essences but not normal runes and so on).
To aid with adding things to those groups, our API provides the following functions:
function Epiphany.API:AddSlotsToSlotGroup(groupName: string, ...: integer)
The vararg contains variants of all the slot machine IDs that should be added to a given group.
Valid slot groups:
Slots
- Conventional slot machinesPain
- Found in sacrifice room Turnover shopsConfessional
Beggars
DevilBeggars
AngelBeggars
ArcadeBeggars
- Found in arcade room Turnover shopsSpecialBeggars
function Epiphany.API:AddHeartsToHeartGroup(groupName: string, ...: integer)
The vararg contains subtypes of all hearts that should be added to a given group.
Valid heart groups:
RedOnly
Angel
Devil
Greedy
function Epiphany.API:AddItemsToCustomPool(poolName: string, ...: integer|table)
The vararg contains item IDs that should be added to a given pool in one of 2 forms:
- Just the item ID as an integer
- A table with the item ID at index [1] and a
Weight
field, where weight is a number. Items with lower weight are less likely to appear, item's default weight is 1.0
Valid custom item pools:
- Turnover shop pools:
VaultShop
DiceShop
BedroomShop
SacRoomShop
PainPool
- Pain machine item payout poolConverterBeggarPool
- Converter beggar item payout poolGlitchSlotPool
- Glitched machine item payout poolGlitchedPocketPool
- Single use pocket items dropped by glitched machine- Surprise Box pools:
SurpriseBox_Poop
SurpriseBox_Heart
SurpriseBox_Coin
SurpriseBox_Bomb
SurpriseBox_Key
SurpriseBox_Battery
SurpriseBox_Card
SurpriseBox_Pill
function Epiphany.API:AddCardsToCardGroup(groupName: string, ...: integer|table)
The vararg contains card IDs that should be added to a given pool in one of 2 forms:
- Just the card ID as an integer
- A table with the card ID at index [1] and a
Weight
field, where weight is a number. Cards with lower weight are less likely to appear, card's default weight is 1.0
Valid card groups:
Tarot
ReverseTarot
Suit
HouseSuit
- A new type of cards introduced by our mod, they are variations of suit cardsUnnus
- Uno cards are canonically named UnnusMono
- Black and White Uno cards introduced by our mod, being the opposite of another Unnus CardMonopoly
Magic
- Magic the Gathering cardsSpecial
Object
Rune
Soul
Holy
DiceCapsule
Essence
All
- While valid, no cards should be added to it, as the only value it contains is -1, which corresponds to a random card.
Note: Trying to add things to an invalid group does not cause errors, but it also won't do anything (save for printing a warning in console), as our mod only interacts with the groups described above.
Adding item IDs to Tarnished Eden's item blacklist
Tarnished Eden has a list of items that he cannot roll into, as well as a list of items he cannot roll into or out of.
Currently, only the list of items he can't roll into is exposed via the API. You can add items to it like this:
Epiphany.API:AddItemsToEdenBlackList(itemid1, itemid2, itemid3)
The items added via this method will be unable to be accessed via Eden's item rerolls. He will, however, still be able to access these items via all other usual methods, such as finding the item in an item room.
Adding custom Turnover shops
Epiphany.API:AddTurnoverShop({
Name = "EXAMPLE_SHOP", -- shop name, is used internally
Checker = function () -- takes 0 arguments, return true if this shop pool should be used
return IsInExampleShop()
end,
ShopLayout =
{
ShopKeeperPosition = Vector(320,180), -- optional, defaults to Vector(320, 200)
SetUpPrice = 15, -- optional, defaults to 10
-- tables at numeric indexes starting from 0 are shop tiers
-- it's possible to have any amount of shop tiers, as long as their indexes are sequential
[0] =
{
SetUpPrice = 5, -- optional, overrides SetUpPrice in outer scope
-- first index is always position, second is the item type
-- pickups are chosen from PickupPool table
{Vector(280, 320), ShopItemType.Pickup},
-- collectibles are picked from current room's item pool
-- unless a getter function is specified at index [3]
{Vector(360, 320), ShopItemType.Collectible},
{Vector(360, 360), ShopItemType.Collectible, function (itemPoolType, rng)
local itemID = rng:RandomInt(CollectibleType.NUM_COLLECTIBLES) + 1
return itemID
end},
-- slots are picked from Slots SlotGroup
-- unless a getter function is specified at index [3]
{Vector(360, 400), ShopItemType.Slot},
{Vector(360, 440), ShopItemType.Slot, function (rng)
local slotID = rng:RandomInt(9) + 1
return slotID
end},
-- restock machine only spawns if turnover is used by TR Keeper with Birthright
{Vector(360, 400), ShopItemType.RestockMachine}
},
[1] =
{
-- ...
},
}
-- PICKUP POOL ENTRY INFO
-- [1] =
-- {
-- [1] - Pickup variant
-- [2] - Pickup subtype
-- * may be an int or a function
-- * that takes 0 arguments and returns the subtype
--
-- minTier - Lowest tier at which a pickup can appear
-- maxTier - Highest tier at which a pickup can appear
-- },
-- Weight - Pickup's weight
-- * Higher weight pickups are more likely to spawn
-- * Pickup's weight is reduced by 4 times for the current shop tier when it spawns
--
-- Pickup variant/subtype are required
-- minTier, maxTier are optional, checks for them pass by default
-- Weight is optional and defaults to 1.0
PickupPool =
{
{{ PickupVariant.PICKUP_BOMB, BombSubType.BOMB_NORMAL, minTier = 0, maxTier = 3 }},
{{ PickupVariant.PICKUP_BOMB, BombSubType.BOMB_DOUBLEPACK, minTier = 2, maxTier = 4 }},
{{ PickupVariant.PICKUP_BOMB, BombSubType.BOMB_GIGA, minTier = 2, maxTier = 4 }, Weight = 0.06},
{{ PickupVariant.PICKUP_BOMB, BombSubType.BOMB_GOLDEN, minTier = 2, maxTier = 4 }, Weight = 0.03},
{{ PickupVariant.PICKUP_KEY, KeySubType.KEY_NORMAL, minTier = 0, maxTier = 3 }},
}
})
Achievement Animations
The Epiphany API has the capability to show any png image as an achievement. There are two different variants of this. They are:
Epiphany.API:ShowTarnishedAchievement("gfx/ui/achievement/example.png")
Epiphany.API:ShowVanillaAchievement("gfx/ui/achievement/example.png")
The Tarnished
variant uses this paper background:
Whereas the Vanilla
variant uses this paper background:
The PNG image used in the function must be placed in the resources
folder of your mod
Tarnished Character Unlock Conventions
Completion Mark Convention
Tarnished Characters unlocks follow a formula similiar to Tainted Characters, however we add our own twist to it. This obviously doesn't need to be exactly followed if you don't want to, but this serves as a framework for those that want to follow our process when designing unlocks They are the following:
Mega Satan
- (Type of) Object, Anything that isn't an item or trinket (slots,mechanics,grid entity,pickups, etc.)Mother
- TrinketBoss Rush & Hush
- EssenceGreedier
- Card (Can be one or multiple)Delirium
- Free Slot, normally a Collectible ItemThe Beast
- Collectible ItemIsaac,Satan,??? and Lamb
- Collectible ItemFull Bulletin
- The Pocket item of the Tarnished (Adapted to work on other characters)Tr Unlock Method
- We try to make a challenge based on the Tainted version of that character, starting inside a normal run and doing specific things, like the unlock method of vanilla forgotten.
Essence/Card Design
Essence try to follow the design of all 3 the iterations of the character they represent The bottle can be shaped however it most suits the character but it should be opaque to not show directly the liquid The liquid should not be seen directly, but it can be hinted at with a puff of smoke coming out of the bottle
"Reversed" Playing Cards that we add are called House Suit Cards internally, Mechanically they follow the same design as Reversed cards, trying to reverse the effect of the card they are based on
Visually they only retain the nomenclature "Name of the Card" + "?"
They use a custom back that can be applied in the entities2.xml as long Epiphany is loaded anm2path="house suit card.anm2"
the front of the card is an Isaac themed card, the 4 Suits are the 4 main pickups of isaac and have a themed "Taint" on them
Clubs are Bombs and have burned/explosion marks left on them
Spades are Keys and have blood splatters
Diamonds are coins and they have a Midas Cursed (Golden) design
Hearts are... hearts, and have a bit rot over them