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 their regular variant. This article will cover setting up this unlock method for your own custommkdo 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 REPENTOGON, 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.
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")localfunctionisFirstPlayerTaintedLocked()localpersistGameData=Isaac.GetPersistentGameData()localplayer=Isaac.GetPlayer()localplayerType=player:GetPlayerType()returnplayerType==MY_CHARandnotpersistGameData:Unlocked(TAINTED_ACHIEVEMENT)end
When entering the closet in Home, the tainted character's body is spawned as part of its room layout if not unlocked. If unlocked, the game will morph it into either Inner Child if that item is unlocked, or a Shopkeeper if not.
The callback MC_PRE_ROOM_ENTITY_SPAWN will trigger when attempting to spawn entities as part of the room layout. Choosing to override it will stop the game from proceeding with any morphs/replacements that would happen otherwise, such as the Inner Child or Shopkeeper.
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.
Inside MC_PRE_ROOM_ENTITY_SPAWN, check the following:
You're on the Home floor.
You're in the closet room.
The entity attempting to be spawned is a "home closet player" slot.
Our isFirstPlayerTaintedLocked check from earlier.
After going through these checks, you can return the exact same entity type, variant, and subtype that was passed by the callback into a table so that the game doesn't attempt to morph it into anything else.
1 2 3 4 5 6 7 8 910111213141516171819
localgame=Game()--The Home floor has a static layout, so the room index of the closet should remain unchanged.localCLOSET_ROOM_INDEX=94localSLOT_HOME_CLOSET_PLAYER=14functionmod:AllowHomeClosetPlayer(entType,variant,subtype,grid,seed)locallevel=game:GetLevel()iflevel:GetStage()==LevelStage.STAGE8--Home.andlevel:GetCurrentRoomIndex()==CLOSET_ROOM_INDEX--Closet.andentType==EntityType.ENTITY_SLOTandvariant==SLOT_HOME_CLOSET_PLAYERandisFirstPlayerTaintedLocked()then--Return its own entity type, variant, and subtype as to not turn into Inner Child or a shopkeeper by the game.return{entType,variant,subtype}endendMod:AddCallback(ModCallbacks.MC_PRE_ROOM_ENTITY_SPAWN,mod.AllowHomeClosetPlayer)
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 upon entering the room containing them. Use MC_POST_NEW_ROOM for entering the room, and Isaac.FindByType to search for all "home closet player" slot machines in the room to update their spritesheet.
1 2 3 4 5 6 7 8 9101112131415161718
localmyCharTaintedSpritePath="gfx/characters/costumes/character_mychar_b.png"--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.functionmod:TryUpdateClosetPlayer()--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.iffirstPlayerTaintedLockedthenlocalsprite=ent:GetSprite()sprite:ReplaceSpritesheet(0,myCharTaintedSpritePath)sprite:LoadGraphics()endendendmod:AddCallback(ModCallbacks.MC_POST_NEW_ROOM,mod.TryUpdateClosetPlayer)
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 won't 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)localMY_CHAR_TAINTED=Isaac.GetPlayerTypeByName("My Character",true)localPLAYER_VARIANT_NORMAL=0localCLOSET_ROOM_INDEX=94localSLOT_HOME_CLOSET_PLAYER=14localisUnlocked=falselocalmyCharTaintedSpritePath="gfx/characters/costumes/character_mychar_b.png"localgame=Game()localfunctionisFirstPlayerTaintedLocked()localplayer=Isaac.GetPlayer()localplayerType=player:GetPlayerType()returnplayerType==MY_CHARandnotisUnlockedendfunctionmod:LockTaintedOnInit(player)ifplayer:GetPlayerType()==MY_CHAR_TAINTEDandnotisUnlockedthenplayer:ChangePlayerType(MY_CHAR)endendmod:AddCallback(ModCallback.MC_POST_PLAYER_INIT,mod.LockTaintedOnInit,PLAYER_VARIANT_NORMAL)functionmod:AllowHomeClosetPlayer(entType,variant,subtype,grid,seed)locallevel=game:GetLevel()iflevel:GetStage()==LevelStage.STAGE8--Home.andlevel:GetCurrentRoomIndex()==CLOSET_ROOM_INDEX--Closet.andentType==EntityType.ENTITY_SLOTandvariant==SLOT_HOME_CLOSET_PLAYERandisFirstPlayerTaintedLocked()then--Return its own entity type, variant, and subtype as to not turn into Inner Child or a shopkeeper by the game.return{entType,variant,subtype}endendMod:AddCallback(ModCallbacks.MC_PRE_ROOM_ENTITY_SPAWN,mod.AllowHomeClosetPlayer)functionmod:TryUpdateClosetPlayer()localfirstPlayerTaintedLocked=isFirstPlayerTaintedLocked()for_,entinipairs(Isaac.FindByType(EntityType.ENTITY_SLOT,SLOT_HOME_CLOSET_PLAYER))doifent.FrameCount==0andfirstPlayerTaintedLockedthenlocalsprite=ent:GetSprite()sprite:ReplaceSpritesheet(0,myCharTaintedSpritePath)sprite:LoadGraphics()endendendmod:AddCallback(ModCallbacks.MC_POST_NEW_ROOM,mod.TryUpdateClosetPlayer)functionmod:UnlockTaintedOnPayPrize()localfirstPlayerTaintedLocked=isFirstPlayerTaintedLocked()for_,entinipairs(Isaac.FindByType(EntityType.ENTITY_SLOT,SLOT_HOME_CLOSET_PLAYER))doiffirstPlayerTaintedLockedthenlocalsprite=ent:GetSprite()ifsprite:IsFinished("PayPrize")thenisUnlocked=truegame:GetHUD():ShowItemText("Tainted MyChar Unlocked!")endendendendmod:AddCallback(ModCallbacks.MC_POST_UPDATE,mod.UnlockTaintedOnPayPrize)