Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions lib/joker_stats.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,96 @@ function MP.STATS.record_match(won)
end

SMODS.save_mod_config(SMODS.Mods["Multiplayer"])

-- Send telemetry to server
local joker_report = MP.STATS.build_joker_report(won)
if #joker_report > 0 then
MP.ACTIONS.match_joker_report(won, joker_report)
end
MP.STATS.reset_lifecycle()
end

function MP.STATS.get_joker_wins(joker_key)
local config = SMODS.Mods["Multiplayer"].config
return config.joker_stats and config.joker_stats[joker_key] or 0
end

-- Joker lifecycle tracking (reset each match)
MP.STATS.joker_lifecycle = {}

--- Call when a joker is added to the player's deck.
--- @param card table -- the Card object reference (used to match on removal)
--- @param key string -- e.g. "j_pizza", "j_mp_speedrun"
--- @param edition string -- e.g. "polychrome", "foil", "none"
--- @param seal string -- e.g. "eternal", "perishable", "none"
--- @param cost number -- gold spent (0 if free)
--- @param source string -- "shop", "booster", "tag", "other"
function MP.STATS.on_joker_acquired(card, key, edition, cost, source)
if not MP.LOBBY.code then return end -- only track in multiplayer
table.insert(MP.STATS.joker_lifecycle, {
card_ref = card,
key = key,
edition = edition or "none",
seal = seal or "none",
cost = cost or 0,
source = source or "other",
ante_acquired = G.GAME.round_resets and G.GAME.round_resets.ante or 1,
ante_removed = nil,
removal_reason = nil,
})
end

--- Call when a joker is removed from the player's deck.
--- Matches by card object reference first, falls back to key match.
--- @param card table -- the Card object being removed
--- @param reason string -- "sold", "destroyed", "perishable"
function MP.STATS.on_joker_removed(card, reason)
if not MP.LOBBY.code then return end
-- First try exact card reference match
for i = #MP.STATS.joker_lifecycle, 1, -1 do
local entry = MP.STATS.joker_lifecycle[i]
if entry.card_ref == card and entry.ante_removed == nil then
entry.ante_removed = G.GAME.round_resets and G.GAME.round_resets.ante or 1
entry.removal_reason = reason
return
end
end
-- Fallback: match by key (for cases where card ref isn't available)
local key = card.config and card.config.center and card.config.center.key
if key then
for i = #MP.STATS.joker_lifecycle, 1, -1 do
local entry = MP.STATS.joker_lifecycle[i]
if entry.key == key and entry.ante_removed == nil then
entry.ante_removed = G.GAME.round_resets and G.GAME.round_resets.ante or 1
entry.removal_reason = reason
return
end
end
end
end

--- Build the match joker report payload.
--- Called from record_match() after the existing local-save logic.
function MP.STATS.build_joker_report(won)
local report = {}
local final_ante = G.GAME.round_resets and G.GAME.round_resets.ante or 1
for _, entry in ipairs(MP.STATS.joker_lifecycle) do
table.insert(report, {
key = entry.key,
edition = entry.edition,
cost = entry.cost,
source = entry.source,
ante_acquired = entry.ante_acquired,
ante_removed = entry.ante_removed, -- nil if held to end
removal_reason = entry.removal_reason, -- nil if held to end
held_at_end = entry.ante_removed == nil,
hold_duration_antes = (entry.ante_removed or final_ante) - entry.ante_acquired,
})
end
return report
end

--- Reset lifecycle tracking for a new match.
function MP.STATS.reset_lifecycle()
MP.STATS.joker_lifecycle = {}
end
14 changes: 14 additions & 0 deletions networking/action_handlers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ end
---@param stake_str string
local function action_start_game(seed, stake_str)
MP.reset_game_states()
MP.STATS.reset_lifecycle()
local stake = tonumber(stake_str)
MP.ACTIONS.set_ante(0)
if not MP.LOBBY.config.different_seeds and MP.LOBBY.config.custom_seed ~= "random" then
Expand Down Expand Up @@ -934,6 +935,19 @@ function MP.ACTIONS.send_game_stats()
action_send_game_stats()
end

function MP.ACTIONS.match_joker_report(won, joker_report)
Client.send({
action = "matchJokerReport",
won = won,
stake = MP.LOBBY.config.stake or 1,
deck = MP.LOBBY.config.back or "Red Deck",
gamemode = MP.LOBBY.config.gamemode or "gamemode_mp_attrition",
ruleset = MP.LOBBY.config.ruleset or "ruleset_mp_blitz",
ante_reached = G.GAME.round_resets and G.GAME.round_resets.ante or 1,
jokers = joker_report,
})
end

function MP.ACTIONS.request_nemesis_stats()
Client.send({
action = "endGameStatsRequested",
Expand Down
10 changes: 10 additions & 0 deletions overrides/game.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ function Card:sell_card()
string.format("Client sent message: action:soldCard,card:%s", self.ability.name),
"MULTIPLAYER"
)
-- Track joker removals for telemetry
if self.config and self.config.center and self.config.center.set == "Joker" then
MP.STATS.on_joker_removed(self, "sold")
end
end
return sell_card_ref(self)
end
Expand Down Expand Up @@ -39,6 +43,12 @@ function G.FUNCS.buy_from_shop(e)
string.format("Client sent message: action:boughtCardFromShop,card:%s,cost:%s", c1.ability.name, c1.cost),
"MULTIPLAYER"
)
-- Track joker acquisitions for telemetry
if c1.config and c1.config.center and c1.config.center.set == "Joker" then
local key = c1.config.center.key
local edition = (c1.edition and c1.edition.type) or "none"
MP.STATS.on_joker_acquired(c1, key, edition, c1.cost, "shop")
end
end
return buy_from_shop_ref(e)
end
Expand Down