FNPreview init

This commit is contained in:
Fantom-Balatro
2025-03-23 04:49:43 -06:00
committed by GitHub
parent f2215714ff
commit e82e03bb90
5 changed files with 481 additions and 0 deletions

14
FNPreview/Card.lua Normal file
View File

@@ -0,0 +1,14 @@
--
-- SMODS compatibility:
--
local orig_get_X_same = get_X_same
function get_X_same(num, hand)
local clean_hand = {}
for _, v in pairs(hand) do
if v.get_id then
table.insert(clean_hand, v)
end
end
return orig_get_X_same(num, clean_hand)
end

258
FNPreview/Core.lua Normal file
View File

@@ -0,0 +1,258 @@
--- Divvy's Preview for Balatro - Core.lua
--
-- The functions responsible for running the simulation at appropriate times;
-- ie. whenever the player modifies card selection or card order.
function DV.PRE.simulate()
-- Guard against simulating in redundant places:
if
not (G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.DRAW_TO_HAND or G.STATE == G.STATES.PLAY_TAROT)
then
return { score = { min = 0, exact = 0, max = 0 }, dollars = { min = 0, exact = 0, max = 0 } }
end
if G.SETTINGS.DV.hide_face_down then
for _, card in ipairs(G.hand.highlighted) do
if card.facing == "back" then
return nil
end
end
if #G.hand.highlighted ~= 0 then
for _, joker in ipairs(G.jokers.cards) do
if joker.facing == "back" then
return nil
end
end
end
end
return DV.SIM.run()
end
--
-- SIMULATION UPDATE ADVICE:
--
function DV.PRE.add_update_event(trigger)
function sim_func()
DV.PRE.data = DV.PRE.simulate()
return true
end
if DV.PRE.enabled() then
G.E_MANAGER:add_event(Event({ trigger = trigger, func = sim_func }))
end
end
-- Update simulation after a consumable (eg. Tarot, Planet) is used:
local orig_use = Card.use_consumeable
function Card:use_consumeable(area, copier)
orig_use(self, area, copier)
DV.PRE.add_update_event("immediate")
end
-- Update simulation after card selection changed:
local orig_hl = CardArea.parse_highlighted
function CardArea:parse_highlighted()
orig_hl(self)
DV.PRE.add_update_event("immediate")
end
-- Update simulation after joker sold:
local orig_card_remove = Card.remove_from_area
function Card:remove_from_area()
orig_card_remove(self)
if self.config.type == "joker" then
DV.PRE.add_update_event("immediate")
end
end
-- Update simulation after joker reordering:
local orig_update = CardArea.update
function CardArea:update(dt)
orig_update(self, dt)
DV.PRE.update_on_card_order_change(self)
end
function DV.PRE.update_on_card_order_change(cardarea)
if
#cardarea.cards == 0
or not (
G.STATE == G.STATES.SELECTING_HAND
or G.STATE == G.STATES.DRAW_TO_HAND
or G.STATE == G.STATES.PLAY_TAROT
)
then
return
end
-- Important not to update on G.STATES.HAND_PLAYED, because it would reset the preview text!
local prev_order = nil
if cardarea.config.type == "joker" and cardarea.cards[1].ability.set == "Joker" then
-- Note that the consumables cardarea also has type 'joker' so must verify by checking first card.
prev_order = DV.PRE.joker_order
elseif cardarea.config.type == "hand" then
prev_order = DV.PRE.hand_order
else
return
end
-- Go through stored card IDs and check against current card IDs, in-order.
-- If any mismatch occurs, toggle flag and update name for next time.
local should_update = false
if #cardarea.cards ~= #prev_order then
prev_order = {}
end
for i, c in ipairs(cardarea.cards) do
if c.sort_id ~= prev_order[i] then
prev_order[i] = c.sort_id
should_update = true
end
end
if should_update then
if cardarea.config.type == "joker" or cardarea.cards[1].ability.set == "Joker" then
DV.PRE.joker_order = prev_order
elseif cardarea.config.type == "hand" then
DV.PRE.hand_order = prev_order
end
DV.PRE.add_update_event("immediate")
end
end
--
-- SIMULATION RESET ADVICE:
--
function DV.PRE.add_reset_event(trigger)
function reset_func()
DV.PRE.data = { score = { min = 0, exact = 0, max = 0 }, dollars = { min = 0, exact = 0, max = 0 } }
return true
end
if DV.PRE.enabled() then
G.E_MANAGER:add_event(Event({ trigger = trigger, func = reset_func }))
end
end
local orig_eval = G.FUNCS.evaluate_play
function G.FUNCS.evaluate_play(e)
orig_eval(e)
DV.PRE.add_reset_event("after")
end
local orig_discard = G.FUNCS.discard_cards_from_highlighted
function G.FUNCS.discard_cards_from_highlighted(e, is_hook_blind)
orig_discard(e, is_hook_blind)
if not is_hook_blind then
DV.PRE.add_reset_event("immediate")
end
end
--
-- USER INTERFACE ADVICE:
--
-- Add animation to preview text:
function G.FUNCS.dv_pre_score_UI_set(e)
local new_preview_text = ""
local should_juice = false
if DV.PRE.data then
if G.SETTINGS.DV.show_min_max and (DV.PRE.data.score.min ~= DV.PRE.data.score.max) then
-- Format as 'X - Y' :
if e.config.id == "dv_pre_l" then
new_preview_text = DV.PRE.format_number(DV.PRE.data.score.min) .. " - "
if DV.PRE.is_enough_to_win(DV.PRE.data.score.min) then
should_juice = true
end
elseif e.config.id == "dv_pre_r" then
new_preview_text = DV.PRE.format_number(DV.PRE.data.score.max)
if DV.PRE.is_enough_to_win(DV.PRE.data.score.max) then
should_juice = true
end
end
else
-- Format as single number:
if e.config.id == "dv_pre_l" then
if G.SETTINGS.DV.show_min_max then
-- Spaces around number necessary to distinguish Min/Max text from Exact text,
-- which is itself necessary to force a HUD update when switching between Min/Max and Exact.
new_preview_text = " " .. DV.PRE.format_number(DV.PRE.data.score.min) .. " "
if DV.PRE.is_enough_to_win(DV.PRE.data.score.min) then
should_juice = true
end
else
new_preview_text = number_format(DV.PRE.data.score.exact)
if DV.PRE.is_enough_to_win(DV.PRE.data.score.exact) then
should_juice = true
end
end
else
new_preview_text = ""
end
end
else
-- Spaces around number necessary to distinguish Min/Max text from Exact text, same as above ^
if e.config.id == "dv_pre_l" then
if G.SETTINGS.DV.show_min_max then
new_preview_text = " ?????? "
else
new_preview_text = "??????"
end
else
new_preview_text = ""
end
end
if (not DV.PRE.text.score[e.config.id:sub(-1)]) or new_preview_text ~= DV.PRE.text.score[e.config.id:sub(-1)] then
DV.PRE.text.score[e.config.id:sub(-1)] = new_preview_text
e.config.object:update_text()
-- Wobble:
if not G.TAROT_INTERRUPT_PULSE then
if should_juice then
G.FUNCS.text_super_juice(e, 5)
e.config.object.colours = { G.C.MONEY }
else
G.FUNCS.text_super_juice(e, 0)
e.config.object.colours = { G.C.UI.TEXT_LIGHT }
end
end
end
end
function G.FUNCS.dv_pre_dollars_UI_set(e)
local new_preview_text = ""
local new_colour = nil
if DV.PRE.data then
if G.SETTINGS.DV.show_min_max and (DV.PRE.data.dollars.min ~= DV.PRE.data.dollars.max) then
if e.config.id == "dv_pre_dollars_top" then
new_preview_text = " " .. DV.PRE.get_sign_str(DV.PRE.data.dollars.max) .. DV.PRE.data.dollars.max
new_colour = DV.PRE.get_dollar_colour(DV.PRE.data.dollars.max)
elseif e.config.id == "dv_pre_dollars_bot" then
new_preview_text = " " .. DV.PRE.get_sign_str(DV.PRE.data.dollars.min) .. DV.PRE.data.dollars.min
new_colour = DV.PRE.get_dollar_colour(DV.PRE.data.dollars.min)
end
else
if e.config.id == "dv_pre_dollars_top" then
local _data = G.SETTINGS.DV.show_min_max and DV.PRE.data.dollars.min or DV.PRE.data.dollars.exact
new_preview_text = " " .. DV.PRE.get_sign_str(_data) .. _data
new_colour = DV.PRE.get_dollar_colour(_data)
else
new_preview_text = ""
new_colour = DV.PRE.get_dollar_colour(0)
end
end
else
new_preview_text = " +??"
new_colour = DV.PRE.get_dollar_colour(0)
end
if not DV.PRE.text.dollars[e.config.id:sub(-3)] or new_preview_text ~= DV.PRE.text.dollars[e.config.id:sub(-3)] then
DV.PRE.text.dollars[e.config.id:sub(-3)] = new_preview_text
e.config.object.colours = { new_colour }
e.config.object:update_text()
if not G.TAROT_INTERRUPT_PULSE then
e.config.object:pulse(0.25)
end
end
end

37
FNPreview/Init.lua Normal file
View File

@@ -0,0 +1,37 @@
--- Divvy's Preview for Balatro - Init.lua
--
-- Global values that must be present for the rest of this mod to work.
if not DV then
DV = {}
end
DV.PRE = {
data = {
score = { min = 0, exact = 0, max = 0 },
dollars = { min = 0, exact = 0, max = 0 },
},
text = {
score = { l = "", r = "" },
dollars = { top = "", bot = "" },
},
joker_order = {},
hand_order = {},
}
DV.PRE._start_up = Game.start_up
function Game:start_up()
DV.PRE._start_up(self)
if not G.SETTINGS.DV then
G.SETTINGS.DV = {}
end
if not G.SETTINGS.DV.PRE then
G.SETTINGS.DV.PRE = true
G.SETTINGS.DV.preview_score = true
G.SETTINGS.DV.preview_dollars = true
G.SETTINGS.DV.hide_face_down = true
G.SETTINGS.DV.show_min_max = true
end
end

131
FNPreview/Interface.lua Normal file
View File

@@ -0,0 +1,131 @@
--- Divvy's Preview for Balatro - Interface.lua
--
-- The user interface components that display simulation results.
-- Append node for preview text to the HUD:
local orig_hud = create_UIBox_HUD
function create_UIBox_HUD()
local contents = orig_hud()
local score_node_wrap = {n=G.UIT.R, config={id = "dv_pre_score_wrap", align = "cm", padding = 0.1}, nodes={}}
if G.SETTINGS.DV.preview_score then table.insert(score_node_wrap.nodes, DV.PRE.get_score_node()) end
table.insert(contents.nodes[1].nodes[1].nodes[4].nodes[1].nodes, score_node_wrap)
local dollars_node_wrap = {n=G.UIT.C, config={id = "dv_pre_dollars_wrap", align = "cm"}, nodes={}}
if G.SETTINGS.DV.preview_dollars then table.insert(dollars_node_wrap.nodes, DV.PRE.get_dollars_node()) end
table.insert(contents.nodes[1].nodes[1].nodes[5].nodes[2].nodes[3].nodes[1].nodes[1].nodes[1].nodes, dollars_node_wrap)
return contents
end
function DV.PRE.get_score_node()
local text_scale = nil
if G.SETTINGS.DV.show_min_max then text_scale = 0.5
else text_scale = 0.75 end
return {n = G.UIT.C, config = {id = "dv_pre_score", align = "cm"}, nodes={
{n=G.UIT.O, config={id = "dv_pre_l", func = "dv_pre_score_UI_set", object = DynaText({string = {{ref_table = DV.PRE.text.score, ref_value = "l"}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, float = true, scale = text_scale})}},
{n=G.UIT.O, config={id = "dv_pre_r", func = "dv_pre_score_UI_set", object = DynaText({string = {{ref_table = DV.PRE.text.score, ref_value = "r"}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, float = true, scale = text_scale})}},
}}
end
function DV.PRE.get_dollars_node()
local top_color = DV.PRE.get_dollar_colour(0)
local bot_color = top_color
if DV.PRE.data ~= nil then
top_color = DV.PRE.get_dollar_colour(DV.PRE.data.dollars.max)
bot_color = DV.PRE.get_dollar_colour(DV.PRE.data.dollars.min)
else
end
return {n=G.UIT.C, config={id = "dv_pre_dollars", align = "cm"}, nodes={
{n=G.UIT.R, config={align = "cm"}, nodes={
{n=G.UIT.O, config={id = "dv_pre_dollars_top", func = "dv_pre_dollars_UI_set", object = DynaText({string = {{ref_table = DV.PRE.text.dollars, ref_value = "top"}}, colours = {top_color}, shadow = true, spacing = 2, bump = true, scale = 0.5})}}
}},
{n=G.UIT.R, config={minh = 0.05}, nodes={}},
{n=G.UIT.R, config={align = "cm"}, nodes={
{n=G.UIT.O, config={id = "dv_pre_dollars_bot", func = "dv_pre_dollars_UI_set", object = DynaText({string = {{ref_table = DV.PRE.text.dollars, ref_value = "bot"}}, colours = {bot_color}, shadow = true, spacing = 2, bump = true, scale = 0.5})}},
}}
}}
end
--
-- SETTINGS:
--
function DV.get_preview_settings_page()
local function preview_score_toggle_callback(e)
if not G.HUD then return end
if G.SETTINGS.DV.preview_score then
-- Preview was just enabled, so add preview node:
G.HUD:add_child(DV.PRE.get_score_node(), G.HUD:get_UIE_by_ID("dv_pre_score_wrap"))
DV.PRE.data = DV.PRE.simulate()
else
-- Preview was just disabled, so remove preview node:
G.HUD:get_UIE_by_ID("dv_pre_score").parent:remove()
end
G.HUD:recalculate()
end
local function preview_dollars_toggle_callback(_)
if not G.HUD then return end
if G.SETTINGS.DV.preview_dollars then
-- Preview was just enabled, so add preview node:
G.HUD:add_child(DV.PRE.get_dollars_node(), G.HUD:get_UIE_by_ID("dv_pre_dollars_wrap"))
DV.PRE.data = DV.PRE.simulate()
else
-- Preview was just disabled, so remove preview node:
G.HUD:get_UIE_by_ID("dv_pre_dollars").parent:remove()
end
G.HUD:recalculate()
end
local function face_down_toggle_callback(_)
if not G.HUD then return end
DV.PRE.data = DV.PRE.simulate()
G.HUD:recalculate()
end
local function minmax_toggle_callback(_)
if not G.HUD or not DV.PRE.enabled() then return end
DV.PRE.data = DV.PRE.simulate()
if G.SETTINGS.DV.preview_score then
if not G.SETTINGS.DV.show_min_max then
-- Min-Max was just disabled, so increase scale:
G.HUD:get_UIE_by_ID("dv_pre_l").config.object.scale = 0.75
G.HUD:get_UIE_by_ID("dv_pre_r").config.object.scale = 0.75
else
-- Min-Max was just enabled, so decrease scale:
G.HUD:get_UIE_by_ID("dv_pre_l").config.object.scale = 0.5
G.HUD:get_UIE_by_ID("dv_pre_r").config.object.scale = 0.5
end
G.HUD:recalculate()
end
end
return
{n=G.UIT.ROOT, config={align = "cm", padding = 0.05, colour = G.C.CLEAR}, nodes={
create_toggle({id = "score_toggle",
label = "Enable Score Preview",
ref_table = G.SETTINGS.DV,
ref_value = "preview_score",
callback = preview_score_toggle_callback}),
create_toggle({id = "dollars_toggle",
label = "Enable Money Preview",
ref_table = G.SETTINGS.DV,
ref_value = "preview_dollars",
callback = preview_dollars_toggle_callback}),
create_toggle({label = "Show Min/Max Preview Instead of Exact",
ref_table = G.SETTINGS.DV,
ref_value = "show_min_max",
callback = minmax_toggle_callback}),
create_toggle({label = "Hide Preview if Any Card is Face-Down",
ref_table = G.SETTINGS.DV,
ref_value = "hide_face_down",
callback = face_down_toggle_callback})
}}
end

41
FNPreview/Utils.lua Normal file
View File

@@ -0,0 +1,41 @@
--- Divvy's Preview for Balatro - Utils.lua
--
-- Utilities for checking states and formatting display.
function DV.PRE.is_enough_to_win(chips)
if G.GAME.blind and
(G.STATE == G.STATES.SELECTING_HAND or
G.STATE == G.STATES.DRAW_TO_HAND or
G.STATE == G.STATES.PLAY_TAROT)
then return (G.GAME.chips + chips >= G.GAME.blind.chips)
else return false
end
end
function DV.PRE.format_number(num)
if not num or type(num) ~= 'number' then return num or '' end
-- Start using e-notation earlier to reduce number length, if showing min and max for preview:
if G.SETTINGS.DV.show_min_max and num >= 1e7 then
local x = string.format("%.4g",num)
local fac = math.floor(math.log(tonumber(x), 10))
return string.format("%.2f",x/(10^fac))..'e'..fac
end
return number_format(num) -- Default Balatro function.
end
function DV.PRE.get_dollar_colour(n)
if n == 0 then return HEX("7e7667")
elseif n > 0 then return G.C.MONEY
elseif n < 0 then return G.C.RED
end
end
function DV.PRE.get_sign_str(n)
if n >= 0 then return "+"
else return "" -- Negative numbers already have a sign
end
end
function DV.PRE.enabled()
return G.SETTINGS.DV.preview_score or G.SETTINGS.DV.preview_dollars
end