TeamREPENTOGON / REPENTOGON

Script extender for The Binding of Isaac: Repentance
https://repentogon.com/
GNU General Public License v2.0
161 stars 18 forks source link

Is it possible to add custom icons in curses.xml? #532

Open BadPig03 opened 3 months ago

BadPig03 commented 3 months ago

It is known that all curses active on the floor, alongside any active mapping effects are shown below the map and represented by their respective icons. However, custom curses can only be rendered using callbacks; they cannot be directly rendered by game. Is is possible to add custom icons in curses.xml and render them without using callbacks?

namishere commented 1 week ago

Need to either figure out a good place to hook minimap render to change the loaded anm2, or reimplement curse rendering

GoldenShit233 commented 1 week ago

Need to either figure out a good place to hook minimap render to change the loaded anm2, or reimplement curse rendering

I wrote some scripts about curse icon rendering

-- Curse HUD Tool
-- Curse text localization
local showStickyStreak = false
function ddad:ShowCurseText(title, subtitle, isSticky, isCurseDisplay)
    if Options.Language ~= "en" and isCurseDisplay then
        for key, value in pairs(self.text.en.curse) do
            if subtitle and subtitle == value.TEXT then
                self.game:GetHUD():ShowItemText(title, self:T("curse." .. key .. ".TEXT"), true)
                if isSticky then
                    showStickyStreak = true
                end
                return false
            end
        end
    end
    if not (isSticky) then
        showStickyStreak = false
    end
end
ddad:AddCallback(ModCallbacks.MC_PRE_ITEM_TEXT_DISPLAY, ddad.ShowCurseText)
function ddad:ShowStreakText()
    if showStickyStreak then
        local sprite = self.game:GetHUD():GetStreakSprite()
        if Input.IsActionPressed(ButtonAction.ACTION_MAP, 0) then
            if sprite:IsPlaying("Text") then
                sprite:Play("TextIn")
            elseif sprite:IsFinished("TextIn") then
                sprite:Play("TextStay")
            end
        else
            sprite:Play("TextOut")
            showStickyStreak = false
        end
    end
end
ddad:AddCallback(ModCallbacks.MC_POST_HUD_RENDER, ddad.ShowStreakText)

-- Curse icon rendering
local modCurseIcon = ddad.LoadNewSprite("gfx/ui/hud_minimap_ddadcurse.anm2") -- my mod's curse icon
if not (MinimapAPI) then
    local curseIcon = Minimap.GetItemIconsSprite() -- in game curse icon
    local curseAlpha = { 1.0, 0 }

    -- get which curse icon should be render
    local function GetCurseList(curse, canSee) 
        curse = curse or ddad.game:GetLevel():GetCurses()
        canSee = canSee or false
        local i = 1
        local list = {}
        -- item icon always render first
        if PlayerManager.AnyoneHasCollectible(CollectibleType.COLLECTIBLE_MIND) then -- mind would over write other 3
            table.insert(list, -6)
        else
            if PlayerManager.AnyoneHasCollectible(CollectibleType.COLLECTIBLE_COMPASS) then
                table.insert(list, -1)
            end
            if PlayerManager.AnyoneHasCollectible(CollectibleType.COLLECTIBLE_BLUE_MAP) then
                table.insert(list, -2)
            end
            if PlayerManager.AnyoneHasCollectible(CollectibleType.COLLECTIBLE_TREASURE_MAP) then
                table.insert(list, -3)
            end
        end
        if PlayerManager.AnyoneHasCollectible(CollectibleType.COLLECTIBLE_RESTOCK) then
            table.insert(list, -5)
        end
        -- in original game, the max count that curse could be render is 7
        -- if the total number exceeds 7, rendering will be stopped directly
        -- the curse icon will be rendered after the item icon, in order of number from smallest to largest
        while curse > 0 and (#list < 7 or not (canSee)) do
            if curse % 2 == 1 then
                table.insert(list, i)
            end
            curse = math.floor(curse * .5)
            i = i + 1
        end
        return list
    end

    -- check if mod curse had be mentioned (if not, just do not change render)
    local function IsModCurse(curseID)
        for _, curse in pairs(ddad.curse) do
            if curse.id == curseID then
                return curse.frame
            end
        end
        return -1
    end

    -- I don't know why I named like this
    -- while open the map, the pos and alpha will change
    -- and more important, in some frame, curse would be render twice
    local function RenderCurseIcons(sprite, basepos)
        local hight = { 47, Minimap.GetDisplayedSize().Y }
        -- can't make sure exactly as same as the original game
        if Minimap.GetState() == MinimapState.NORMAL then
            curseAlpha[1] = math.min(curseAlpha[1] + .125, 1)
            curseAlpha[2] = math.max(curseAlpha[2] - .125, 0)
        elseif Minimap.GetState() == MinimapState.EXPANDED then
            curseAlpha[1] = math.max(curseAlpha[1] - .125, 0)
            curseAlpha[2] = math.min(curseAlpha[2] + .100, .8)
        else
            curseAlpha[1] = math.max(curseAlpha[1] - .125, 0)
            curseAlpha[2] = math.min(curseAlpha[2] + .125, 1)
        end
        if curseAlpha[1] > 0 or hight[2] == hight[1] then
            sprite.Color = Color(1, 1, 1, curseAlpha[1])
            sprite:Render(basepos + Vector(0, hight[1]))
        end
        if hight[2] ~= hight[1] and curseAlpha[2] > 0 then
            sprite.Color = Color(1, 1, 1, curseAlpha[2])
            sprite:Render(basepos + Vector(0, hight[2]))
        end
    end

    -- main function
    function ddad:RenderCurseIcon()
        -- get icons that should be render
        local curseList = GetCurseList(nil, true)
        local hasModCurse = false
        local sholdRenderDefalt = false
        for _, curse in ipairs(curseList) do
            if IsModCurse(curse) >= 0 then
                hasModCurse = true
                break
            end
        end
        -- reset curse icon offset
        --[[
            why?
            here is a bug!
            when game trying to render the first mod curse, would unanticipately render Curse of the Giant!
            so when curse 9 will appear, set its offset to -9999 so that it won't be on the screen
            for other mod curse icon, would render blank
        ]]
        curseIcon.Offset = Vector(0, 0)
        if hasModCurse then
            -- base pos (top right)
            local pos = Vector(Isaac.GetScreenWidth(), 0) + Options.HUDOffset * Vector(-24, 14) + Vector(-11, 10) + Minimap.GetShakeOffset()
            -- the gap between icons varies depending on the number of icons
            local offset = 16 - math.max(#curseList - 3, 0)
            -- would have that bug
            sholdRenderDefalt = self.FindItemInA(curseList, 9)
            if sholdRenderDefalt then
                for index, curse in ipairs(curseList) do
                    -- original game icon
                    if curse <= 8 then
                        -- negative values correspond to item icon
                        if curse < 0 then
                            curseIcon:Play("icons")
                        -- positive values correspond to curse icon
                        else
                            curseIcon:Play("curses")
                        end
                        curseIcon:SetFrame(math.abs(curse) - 1)
                        RenderCurseIcons(curseIcon, pos + Vector(-offset, 0) * (index - 1))
                    end
                end
                curseIcon.Offset = Vector(0, -9999)
            end
            -- mod curse icon
            for index, curse in ipairs(curseList) do
                local frame = IsModCurse(curse)
                if frame >= 0 then
                    modCurseIcon:SetFrame(frame)
                    RenderCurseIcons(modCurseIcon, pos + Vector(-offset, 0) * (index - 1))
                end
            end
        end
    end

    ddad:AddCallback(ModCallbacks.MC_HUD_RENDER, ddad.RenderCurseIcon)
else
    -- has MinimapAPI mod
    for key, value in pairs(ddad.curse) do
        MinimapAPI:AddMapFlag(
            "ddad_curse_" .. key,
            function() return ddad.game:GetLevel():GetCurses() & value.levelCurse ~= 0 end,
            modCurseIcon, "Idle", value.frame
        )
    end
end

I am not good at programming and English, please forgive me if there is any problem