Tainted characters are alternate, twisted versions of their regular variants that are unlocked by finding them inside the Home floor's secret closet while playing as said regular variant. This article will cover setting up this unlock method for your own custom tainted character.
A boolean value should be saved to remember the state of the unlock, for whether the tainted character is unlocked or not. With REPENTOGON, you can create an achievement and attach it to your tainted character directily in the players.xml file. Without REPENTOGON, you will need to learn how to manually handle save data.
Before the tainted character can be unlocked, it must be locked and unable to be played. As mentioned previously, REPENTOGON makes this process simple by allowing you to attach an achievement to the character, which will stop your character from being selected on the character selection menu. Without it, there are no capabilities to lock the character inside the main menu. Instead, the character will need to be changed to a different character when being initialized.
EntityPlayer:ChangePlayerType will be utilized in order to change from your tainted character to your regular character. Using this function within MC_POST_PLAYER_INIT right as the player spawns will also grant the character any items that they may have on them as defined in their players.xml file. Depending on how your character is setup, you may need to make some additional adjustments to ensure this alternate way of starting as your regular character isn't any different from starting as them from the character selection menu.
1 2 3 4 5 6 7 8 9101112131415161718192021
localmod=RegisterMod("My Mod",1)--PlayerTypes of the regular and tainted version of your characterlocalMY_CHAR=Isaac.GetPlayerTypeByName("My Character",false)localMY_CHAR_TAINTED=Isaac.GetPlayerTypeByName("My Character",true)localPLAYER_VARIANT_NORMAL=0--This is a stand-in for however you have your save data structured and where you keep your variable for tracking the tainted unlock.localisUnlocked=false--MC_POST_PLAYER_INIT passes the player being initialized.functionmod:LockTaintedOnInit(player)--Our tainted isn't unlocked yet!ifplayer:GetPlayerType()==MY_CHAR_TAINTEDandnotisUnlockedthenplayer:ChangePlayerType(MY_CHAR)--Insert other necessary adjustments hereendend--MC_POST_PLAYER_INIT triggers whenever a player is initialized at run start, co-op spawn, run continue, or Genesis.--PLAYER_VARIANT_NORMAL is inserted as the optional argument to ensure it runs for regular players and not co-op babies.mod:AddCallback(ModCallback.MC_POST_PLAYER_INIT,mod.LockTaintedOnInit,PLAYER_VARIANT_NORMAL)
The traditional method of unlocking a tainted character is by locating and touching their shaking body within the closet room of the Home floor. To start, check that you're entering the correct room with your character. The tainted character unlock method only looks at the first player using Isaac.GetPlayer().
You will be checking the first player's character multiple times throughout this tutorial. For convenience, create a function for it and return if the player is your character and has their tainted character locked.
1 2 3 4 5 6 7 8 910111213141516171819202122232425
localgame=Game()--The Home floor has a static layout, so the room index of the closet should remain unchanged.localCLOSET_ROOM_INDEX=94localfunctionisFirstPlayerTaintedLocked()localplayer=Isaac.GetPlayer()localplayerType=player:GetPlayerType()returnplayerType==MY_CHARandnotisUnlockedendfunctionmod:SpawnTaintedOnClosetEnter()--Local variables for convenience.localroom=game:GetRoom()locallevel=game:GetLevel()iflevel:GetStage()==LevelStage.STAGE8--Home floorandlevel:GetCurrentRoomIndex()==CLOSET_ROOM_INDEXandroom:IsFirstVisit()--We only need to spawn the body once.andisFirstPlayerTaintedLocked()thenendendmod:AddCallback(ModCallbacks.MC_POST_NEW_ROOM,mod.SpawnTaintedOnClosetEnter)
If using REPENTOGON, the isFirstPlayerTaintedLocked check should specifically involve the achievement attached to your character. As such, the function should be adjusted like so:
123456789
--This is the achievement defined in achievements.xml and attached to your tainted character in players.xmllocalTAINTED_ACHIEVEMENT=Isaac.GetAchievementIdByName("Tainted MyChar")localpersistGameData=Isaac.GetPersistentGameData()localfunctionisFirstPlayerTaintedLocked()localplayer=Isaac.GetPlayer()localplayerType=player:GetPlayerType()returnplayerType==MY_CHARandnotpersistGameData:Unlocked(TAINTED_ACHIEVEMENT)end
Next, the room should be cleared of any extra entities and have the player body spawned. For all modded characters, tainted characters, and for vanilla characters that have their tainted variants unlocked, Inner Child will spawn if it is unlocked. If Inner Child is not unlocked, a shopkeeper will spawn instead. These are the only entities that need to be removed. For the player body, it is internally a slot machine (EntityType.ENTITY_SLOT) with a variant of 14. With REPENTOGON, there is a SlotVariant enum with 14 being assigned to SlotVariant.HOME_CLOSET_PLAYER.
localgame=Game()localCLOSET_ROOM_INDEX=94localSLOT_HOME_CLOSET_PLAYER=14functionmod:SpawnTaintedOnClosetEnter()localroom=game:GetRoom()locallevel=game:GetLevel()iflevel:GetStage()==LevelStage.STAGE8andlevel:GetCurrentRoomIndex()==CLOSET_ROOM_INDEXandroom:IsFirstVisit()andisFirstPlayerTaintedLocked()then--Locate the first instance of an Inner Child collectible and ShopkeeperlocalinnerChild=Isaac.FindByType(EntityType.ENTITY_PICKUP,PickupVariant.PICKUP_COLLECTIBLE,CollectibleType.COLLECTIBLE_INNER_CHILD)[1]localshopKeeper=Isaac.FindByType(EntityType.ENTITY_SHOPKEEPER)[1]--Remove Inner Child if found.ifinnerChildtheninnerChild:Remove()--A shopkeeper will only spawn if Inner Child isn't unlocked. If found, remove it.elseifshopKeeperthenshopKeeper:Remove()end--Game():Spawn(EntityType, integer Variant, Vector Position, Vector Velocity, Entity SpawnerEntity, integer SubType, integer Seed)game:Spawn(EntityType.ENTITY_SLOT,SLOT_HOME_CLOSET_PLAYER,room:GetCenterPos(),Vector.Zero,nil,0,Random())endendmod:AddCallback(ModCallbacks.MC_POST_NEW_ROOM,mod.SpawnTaintedOnClosetEnter)
The player body will attempt to take on the tainted appearance of the first player's current character. If no tainted is available, or when playing a modded character, it will spawn with the first player's own spritesheet. As such, the spritesheet must be manually updated to display the tainted's spritesheet.
Without REPENTOGON, there are no callbacks for slot machines, so they must be manually searched for after they are spawned in and upon re-entering the room. The spritesheet to use must also be manually typed out as a string.
localmyCharTaintedSpritePath="gfx/characters/costumes/character_mychar_b.png"localfunctiontryUpdateClosetIsaac()--We store this check before the FindByType loop as we only need to check it once.localfirstPlayerTaintedLocked=isFirstPlayerTaintedLocked()--Search for all slot machines with the desired variant.for_,entinipairs(Isaac.FindByType(EntityType.ENTITY_SLOT,SLOT_HOME_CLOSET_PLAYER))do--Check that it just spawned and we should update it for our character.ifent.FrameCount==0andfirstPlayerTaintedLockedthenlocalsprite=ent:GetSprite()sprite:ReplaceSpritesheet(0,myCharTaintedSpritePath)sprite:LoadGraphics()endendend--Continuing with the mod:SpawnTaintedOnClosetEnter() function from earlier:functionmod:SpawnTaintedOnClosetEnter()--Don't actually type this part out, its just for reference.if...then--Removed Inner Child or Shopkeeper--Spawned Closet playerend--We want to update the body's sprite no matter what room it's located in, so this will activate on every MC_POST_NEW_ROOM.tryUpdateClosetIsaac()end
If you have REPENTOGON, you can update the spritesheet whenever the slot machine initializes on MC_POST_SLOT_INIT. For additional convenience, you can automatically fetch your tainted counterpart's spritesheet using EntityConfigPlayer.
--Fetches the tainted spritesheet of the passed player.localfunctiongetTaintedSpritesheet(player)--Current player's config.localplayerConfig=player:GetEntityConfigPlayer()--Get their tainted's config. Keep in mind that on a tainted, it will return the regular counterpart.localtaintedConfig=playerConfig:GetTaintedCounterpart()--If already a tainted character or has no tainted, returns the current player's spritesheet.ifplayerConfig:IsTainted()ornottaintedConfigthen--Returning early stops later code in this function from running.returnplayerConfig:GetSkinPath()end--Return the tainted character's spritesheet path.returntaintedConfig:GetSkinPath()endfunctionmod:OnClosetIsaacInit(slot)ifisFirstPlayerTaintedLocked()thenlocalsprite=slot:GetSprite()localplayer=Isaac.GetPlayer()localspritesheet=getTaintedSpritesheet(player)--Update the spritesheet. The last `true` here introduced by REPENTOGON will trigger `sprite:LoadGraphics()`.sprite:ReplaceSpritesheet(0,spritesheet,true)endendmod:AddCallback(ModCallbacks.MC_POST_SLOT_INIT,mod.OnClosetIsaacInit,SlotVariant.HOME_CLOSET_PLAYER)
The same method of finding the player body last time will be used here once more. This time, MC_POST_UPDATE is used as the animation must be constantly checked for when it finishes. If you wish to show an achievement paper, that must be handled manually and is unable to be covered in this tutorial. Otherwise, you can use something such as HUD:ShowItemText to communicate to the player that the character was unlocked.
REPENTOGON's MC_POST_SLOT_UPDATE will pass slot machines being updated, skipping the need for Isaac.FIndByType from the non-REPENTOGON method. You can unlock your registered achievement when the PayPrize animation finishes.
localmod=RegisterMod("My Mod",1)localMY_CHAR=Isaac.GetPlayerTypeByName("My Character",false)localTAINTED_ACHIEVEMENT=Isaac.GetAchievementIdByName("Tainted MyChar")localCLOSET_ROOM_INDEX=94localgame=Game()localpersistGameData=Isaac.GetPersistentGameData()localfunctionisFirstPlayerTaintedLocked()localplayer=Isaac.GetPlayer()localplayerType=player:GetPlayerType()returnplayerType==MY_CHARandnotpersistGameData:Unlocked(TAINTED_ACHIEVEMENT)endlocalfunctiongetTaintedSpritesheet(player)localplayerConfig=player:GetEntityConfigPlayer()localtaintedConfig=playerConfig:GetTaintedCounterpart()ifplayerConfig:IsTainted()ornottaintedConfigthenreturnplayerConfig:GetSkinPath()endreturntaintedConfig:GetSkinPath()endfunctionmod:SpawnTaintedOnClosetEnter()localroom=game:GetRoom()locallevel=game:GetLevel()iflevel:GetStage()==LevelStage.STAGE8andlevel:GetCurrentRoomIndex()==CLOSET_ROOM_INDEXandroom:IsFirstVisit()andisFirstPlayerTaintedLocked()thenlocalinnerChild=Isaac.FindByType(EntityType.ENTITY_PICKUP,PickupVariant.PICKUP_COLLECTIBLE,CollectibleType.COLLECTIBLE_INNER_CHILD)[1]localshopKeeper=Isaac.FindByType(EntityType.ENTITY_SHOPKEEPER)[1]ifinnerChildtheninnerChild:Remove()elseifshopKeeperthenshopKeeper:Remove()endgame:Spawn(EntityType.ENTITY_SLOT,SlotVariant.HOME_CLOSET_PLAYER,room:GetCenterPos(),Vector.Zero,nil,0,Random())endendmod:AddCallback(ModCallbacks.MC_POST_NEW_ROOM,mod.SpawnTaintedOnClosetEnter)functionmod:OnClosetIsaacInit(slot)ifisFirstPlayerTaintedLocked()thenlocalsprite=slot:GetSprite()localplayer=Isaac.GetPlayer()localspritesheet=getTaintedSpritesheet(player)--Update the spritesheet. The last `true` here introduced by REPENTOGON will trigger `sprite:LoadGraphics()`.sprite:ReplaceSpritesheet(0,spritesheet,true)endendmod:AddCallback(ModCallbacks.MC_POST_SLOT_INIT,mod.OnClosetIsaacInit,SlotVariant.HOME_CLOSET_PLAYER)functionmod:UnlockTaintedOnPayPrize(slot)ifisFirstPlayerTaintedLocked()thenlocalsprite=slot:GetSprite()ifsprite:IsFinished("PayPrize")thenpersistGameData:TryUnlock(TAINTED_ACHIEVEMENT)endendendmod:AddCallback(ModCallbacks.MC_POST_SLOT_UPDATE,mod.UnlockTaintedOnPayPrize,SlotVariant.HOME_CLOSET_PLAYER)