REPENTOGON icon indicating copy to clipboard operation
REPENTOGON copied to clipboard

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

Open BadPig03 opened this issue 1 year ago • 2 comments

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?

BadPig03 avatar Aug 27 '24 13:08 BadPig03

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

namishere avatar Nov 16 '24 06:11 namishere

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

GoldenShit233 avatar Nov 16 '24 08:11 GoldenShit233

Yes 46b8ac78c476

epfly6 avatar Oct 11 '25 19:10 epfly6