Files
PyTaikoGreen/Scripts/result/result_player.lua
T
TaikoZhongandClaude Fable 5.1 dd036cb85e result: clear state and clear-zone art follow the engine's norma
gauge_state came from a cell count (68 of 87 for oni) while the game clears on soul >= norma (6000/7000/7000/8000); in the band just below the norma the result lit the gold cell, the clear caption and a crown for a failed run. Hard also used the oni layout although the HUD gives it normal's zone and norma. Derive the state from the soul value, pick the art tier like the HUD, and light the gold cell and caption only when cleared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 12:18:31 +08:00

401 lines
19 KiB
Lua

ResultPlayer = {}
ResultPlayer.__index = ResultPlayer
function ResultPlayer.new(player_num, is_2p, has_2p,
gauge_length, difficulty, selected_difficulty,
crown_type, score_diff, score_method,
mod_display, mod_inverse, mod_random, mod_speed)
local self = setmetatable({}, ResultPlayer)
tex.load_animations("result")
self.player_num = player_num
self.is_2p = is_2p
self.has_2p = has_2p
self.gauge_length = gauge_length
self.difficulty = difficulty
self.selected_difficulty = selected_difficulty
self.crown_type = crown_type
self.score_diff = score_diff
self.score_method = score_method
self.mod_display = mod_display
self.mod_inverse = mod_inverse
self.mod_random = mod_random
self.mod_speed = mod_speed
-- Clear follows the engine's gauge (Gauge::get_is_clear: soul >= norma), not a cell
-- count: the norma is easy 6000 / normal 7000 / hard 7000 / oni+ura 8000 of 10000 soul,
-- and gauge_length arrives as soul / 10000 * 87. Hard shares normal's norma and so, like
-- the in-game HUD, normal's clear-zone art (zone at cell 60), not the oni layout.
local ART = { [0] = 0, [1] = 1, [2] = 1, [3] = 2 }
local NORMA = { [0] = 6000, [1] = 7000, [2] = 7000, [3] = 8000 }
local d = math.min(math.max(difficulty, 0), 3)
self.art_index = ART[d]
if gauge_length == 87 then
self.gauge_state = 2
elseif gauge_length / 87.0 * 10000.0 >= NORMA[d] - 0.5 then
self.gauge_state = 1
else
self.gauge_state = 0
end
if self.art_index >= 2 then
self.diff_str = "_hard"
elseif self.art_index >= 1 then
self.diff_str = "_normal"
else
self.diff_str = "_easy"
end
local p = tostring(player_num) .. "p"
self.player_str = p
self.score = ""
self.good = ""
self.ok = ""
self.bad = ""
self.max_combo = ""
self.total_drumroll = ""
self.fade_in_finished_flag = false
self.gauge_started = false
self.score_delay = nil
self.fade_in_started = false
self.rainbow_start_ms = -1.0
self.rainbow_frac = 0.0
-- Sub-objects
self.bottom_chars = BottomCharacters.new()
self.crown_obj = nil
self.crown_msg_obj = nil
self.high_score_obj = nil
-- Animations owned by ResultPlayer
self.fade_in_bottom = tex.get_animation(1, "result")
self.gauge_fade_in = tex.get_animation(17, "result")
self.tamashii_fire = tex.get_animation(20, "result")
self.t_grad_clear = tex.load_texture("result/background/gradient_clear")
self.t_grad_fail = tex.load_texture("result/background/gradient_fail")
self.t_overlay = tex.load_texture("result/score/overlay")
self.t_difficulty = tex.load_texture("result/score/difficulty")
self.t_judge_good = tex.load_texture("result/score/judge_good")
self.t_judge_ok = tex.load_texture("result/score/judge_ok")
self.t_judge_bad = tex.load_texture("result/score/judge_bad")
self.t_judge_num = tex.load_texture("result/score/judge_num")
self.t_max_combo = tex.load_texture("result/score/max_combo")
self.t_drumroll = tex.load_texture("result/score/drumroll")
self.t_score_lbl = tex.load_texture("result/score/score")
self.t_score_shinuchi_lbl = tex.load_texture("result/score/score_shinuchi")
self.t_score_num = tex.load_texture("result/score/score_num")
self.t_mod_doron = tex.load_texture("result/score/mod_doron")
self.t_mod_abekobe = tex.load_texture("result/score/mod_abekobe")
self.t_mod_kimagure = tex.load_texture("result/score/mod_kimagure")
self.t_mod_detarame = tex.load_texture("result/score/mod_detarame")
self.t_mod_yonbai = tex.load_texture("result/score/mod_yonbai")
self.t_mod_sanbai = tex.load_texture("result/score/mod_sanbai")
self.t_mod_baisaku = tex.load_texture("result/score/mod_baisaku")
self.t_gauge_unfilled = tex.load_texture("result/gauge/" .. p .. "_unfilled" .. self.diff_str)
self.t_gauge_bar = tex.load_texture("result/gauge/" .. p .. "_bar")
self.t_gauge_rainbow = tex.load_texture("result/gauge/rainbow" .. self.diff_str)
self.t_gauge_overlay = tex.load_texture("result/gauge/overlay" .. self.diff_str)
self.t_gauge_footer = tex.load_texture("result/gauge/footer")
self.t_gauge_clear = tex.load_texture("result/gauge/clear")
self.t_gauge_clear_dark = tex.load_texture("result/gauge/clear_dark")
self.t_gauge_bar_clear_transition = tex.load_texture("result/gauge/bar_clear_transition")
self.t_gauge_bar_clear_top = tex.load_texture("result/gauge/bar_clear_top")
self.t_gauge_bar_clear_bottom = tex.load_texture("result/gauge/bar_clear_bottom")
self.t_gauge_tamashii = tex.load_texture("result/gauge/tamashii")
self.t_gauge_tamashii_dark = tex.load_texture("result/gauge/tamashii_dark")
self.t_gauge_tamashii_fire = tex.load_texture("result/gauge/tamashii_fire")
self.t_gauge_tamashii_overlay = tex.load_texture("result/gauge/tamashii_overlay")
local bar_info = tex.get_texture_info("gauge", p .. "_bar")
self.gauge_bar_width = bar_info and bar_info.width or 0
local si = tex.get_skin_config("score_info_counter_margin")
self.sc_score_info_margin = si and si.x or 0
local rs = tex.get_skin_config("result_score_margin")
self.sc_result_score_margin = rs and rs.x or 0
local r2 = tex.get_skin_config("result_2p_offset")
self.sc_result_2p_offset = r2 and r2.y or 0
return self
end
function ResultPlayer:update(current_ms, fade_in_finished,
score, good, ok, bad, max_combo, total_drumroll)
self.fade_in_finished_flag = fade_in_finished
self.score = score
self.good = good
self.ok = ok
self.bad = bad
self.max_combo = max_combo
self.total_drumroll = total_drumroll
if fade_in_finished and not self.gauge_started then
self.gauge_started = true
self.gauge_fade_in:start()
self.tamashii_fire:start()
self.bottom_chars:start()
end
if self.gauge_started then
if self.gauge_state == 2 then
if self.rainbow_start_ms < 0 then self.rainbow_start_ms = current_ms end
self.rainbow_frac = math.fmod((current_ms - self.rainbow_start_ms) / 75.0, 8.0)
end
self.tamashii_fire:update(current_ms)
self.gauge_fade_in:update(current_ms)
if self.gauge_fade_in.is_finished and self.score_delay == nil then
self.score_delay = current_ms + 1883
end
end
if self.score_delay ~= nil and current_ms > self.score_delay and not self.fade_in_started then
self.fade_in_started = true
self.fade_in_bottom:start()
end
self.fade_in_bottom:update(current_ms)
if self.fade_in_started then
self.bottom_chars:update(current_ms, self.gauge_state)
end
if self.bottom_chars:is_finished() then
if self.gauge_state >= 1 and not self.crown_obj then
self.crown_obj = ResultCrown.new(self.crown_type, self.is_2p)
if self.crown_type == 1 or self.crown_type == 2 then
audio.play_sound("full_combo_voice_" .. tostring(self.player_num) .. "p", "voice")
end
elseif self.gauge_state == 0 and not self.crown_msg_obj then
local frame = (self.gauge_length < 87.0 / 2.0) and 0 or 1
self.crown_msg_obj = ResultCrownMessage.new(frame, self.is_2p)
if frame == 0 then
audio.play_sound("max_fail_voice_" .. tostring(self.player_num) .. "p", "voice")
else
audio.play_sound("fail_voice_" .. tostring(self.player_num) .. "p", "voice")
end
end
end
if self.crown_obj then
self.crown_obj:update(current_ms)
if self.crown_obj:is_settled() and not self.crown_msg_obj then
local frame = (self.gauge_state == 2) and 3 or 2
self.crown_msg_obj = ResultCrownMessage.new(frame, self.is_2p)
if frame == 2 then
audio.play_sound("clear_voice_" .. tostring(self.player_num) .. "p", "voice")
else
audio.play_sound("max_clear_voice_" .. tostring(self.player_num) .. "p", "voice")
end
end
end
if self.crown_msg_obj then
self.crown_msg_obj:update(current_ms)
end
if not self.high_score_obj and self.score_diff > 0 and self.good ~= "" then
self.high_score_obj = HighScoreIndicator.new(self.score_diff, self.is_2p)
end
if self.high_score_obj then
self.high_score_obj:update(current_ms)
end
end
function ResultPlayer:draw()
local p_idx = self.is_2p and 1 or 0
if self.fade_in_started then
local fade_val = math.min(0.4, self.fade_in_bottom.attribute)
if self.is_2p then
if self.gauge_state == 0 then
if self.t_grad_fail then tex.draw_texture(self.t_grad_fail, { fade = fade_val }) end
else
if self.t_grad_clear then tex.draw_texture(self.t_grad_clear, { fade = fade_val }) end
end
else
local y = (self.sc_result_2p_offset ~= 0) and (self.has_2p and 1.0 or 0.0) or 0.0
if self.gauge_state == 0 then
if self.t_grad_fail then tex.draw_texture(self.t_grad_fail, { y = y, fade = fade_val }) end
else
if self.t_grad_clear then tex.draw_texture(self.t_grad_clear, { y = y, fade = fade_val }) end
end
end
end
if self.t_overlay then tex.draw_texture(self.t_overlay, { color = { 255, 255, 255, 192 }, index = p_idx }) end
if self.t_difficulty then tex.draw_texture(self.t_difficulty, { frame = self.selected_difficulty, index = p_idx }) end
if not self.has_2p then self.bottom_chars:draw() end
if self.t_judge_good then tex.draw_texture(self.t_judge_good, { index = p_idx }) end
if self.t_judge_ok then tex.draw_texture(self.t_judge_ok, { index = p_idx }) end
if self.t_judge_bad then tex.draw_texture(self.t_judge_bad, { index = p_idx }) end
if self.t_max_combo then tex.draw_texture(self.t_max_combo, { index = p_idx }) end
if self.t_drumroll then tex.draw_texture(self.t_drumroll, { index = p_idx }) end
self:draw_score_info()
self:draw_total_score()
if self.crown_obj then self.crown_obj:draw() end
if self.crown_msg_obj then self.crown_msg_obj:draw() end
self:draw_modifiers()
if self.high_score_obj then self.high_score_obj:draw() end
end
function ResultPlayer:draw_gauge()
if not self.gauge_started then return end
local p_idx = self.is_2p and 1 or 0
local scale = 10.0 / 11.0
local base_fade = self.gauge_fade_in.attribute
local gl_int = math.floor(self.gauge_length)
local clear_pts = { 52, 60, 69 } -- easy / normal+hard / oni norma cells (of 87)
local clear_pt = clear_pts[self.art_index + 1]
local bar_w = self.gauge_bar_width
if self.t_gauge_unfilled then
tex.draw_texture(self.t_gauge_unfilled, { scale = scale, fade = base_fade, index = p_idx })
end
if self.gauge_state == 2 then
local fa = math.floor(self.rainbow_frac) % 8
local fb = (fa + 1) % 8
local t = self.rainbow_frac - math.floor(self.rainbow_frac)
if self.t_gauge_rainbow then
tex.draw_texture(self.t_gauge_rainbow, { frame = fa, scale = scale, fade = base_fade, index = p_idx })
tex.draw_texture(self.t_gauge_rainbow, { frame = fb, scale = scale, fade = base_fade * t, index = p_idx })
end
else
if self.t_gauge_bar then
local x2 = math.min(gl_int * bar_w, (clear_pt - 1) * bar_w) - bar_w
tex.draw_texture(self.t_gauge_bar, { scale = scale, x2 = x2, fade = base_fade, index = p_idx })
end
-- The transition piece is the first gold cell (the rounded cap of the clear zone).
-- Light it only when the run is cleared, like the HUD gauge (Gauge::get_is_clear):
-- the norma falls mid-cell (oni 8000 = cell 69.6), so a cell-count test lit it for
-- a failed run at 7816..7999 soul.
if gl_int >= clear_pt - 1 and self.gauge_state >= 1 and self.t_gauge_bar_clear_transition then
tex.draw_texture(self.t_gauge_bar_clear_transition,
{ scale = scale, x = (clear_pt - 1) * bar_w, fade = base_fade, index = p_idx })
end
if gl_int > clear_pt and self.gauge_state >= 1 then
local extra = (gl_int - clear_pt) * bar_w
if self.t_gauge_bar_clear_top then
tex.draw_texture(self.t_gauge_bar_clear_top,
{ scale = scale, x = clear_pt * bar_w, x2 = extra, fade = base_fade, index = p_idx })
end
if self.t_gauge_bar_clear_bottom then
tex.draw_texture(self.t_gauge_bar_clear_bottom,
{ scale = scale, x = clear_pt * bar_w, x2 = extra, fade = base_fade, index = p_idx })
end
end
end
if self.t_gauge_overlay then
tex.draw_texture(self.t_gauge_overlay,
{ scale = scale, fade = math.min(0.15, base_fade), index = p_idx })
end
if self.t_gauge_footer then
tex.draw_texture(self.t_gauge_footer, { scale = scale, fade = base_fade, index = p_idx })
end
local clear_idx = self.art_index + p_idx * 3
-- A failed run never lights the clear caption, even when the fill touches the zone.
if gl_int >= clear_pt - 1 and self.gauge_state >= 1 then
if self.t_gauge_clear then
tex.draw_texture(self.t_gauge_clear,
{ scale = scale, fade = base_fade, index = clear_idx })
end
if self.gauge_state == 2 and self.t_gauge_tamashii_fire then
local fire_f = math.floor(self.tamashii_fire.attribute)
tex.draw_texture(self.t_gauge_tamashii_fire,
{ frame = fire_f, scale = 0.75 * scale, center = true, fade = base_fade, index = p_idx })
end
if self.t_gauge_tamashii then
tex.draw_texture(self.t_gauge_tamashii, { scale = scale, fade = base_fade, index = p_idx })
end
if self.gauge_state == 2 and self.t_gauge_tamashii_overlay then
local f = math.floor(self.tamashii_fire.attribute)
if f == 0 or f == 1 or f == 4 or f == 5 then
tex.draw_texture(self.t_gauge_tamashii_overlay,
{ scale = scale, fade = math.min(0.5, base_fade), index = p_idx })
end
end
else
if self.t_gauge_clear_dark then
tex.draw_texture(self.t_gauge_clear_dark,
{ scale = scale, fade = base_fade, index = clear_idx })
end
if self.t_gauge_tamashii_dark then
tex.draw_texture(self.t_gauge_tamashii_dark,
{ scale = scale, fade = base_fade, index = p_idx })
end
end
end
function ResultPlayer:draw_score_info()
local scores = { self.good, self.ok, self.bad, self.max_combo, self.total_drumroll }
local p_idx = self.is_2p and 1 or 0
for j, s in ipairs(scores) do
if s ~= "" and self.t_judge_num then
local n = math.floor(tonumber(s) or 0)
local str = tostring(n)
for i = 1, #str do
local d = tonumber(string.sub(str, #str - i + 1, #str - i + 1)) or 0
tex.draw_texture(self.t_judge_num, {
frame = d,
x = -((i - 1) * self.sc_score_info_margin),
index = (j - 1) + p_idx * 5,
})
end
end
end
end
function ResultPlayer:draw_total_score()
if not self.fade_in_finished_flag then return end
local p_idx = self.is_2p and 1 or 0
if self.score_method then
if self.t_score_shinuchi_lbl then tex.draw_texture(self.t_score_shinuchi_lbl, { index = p_idx }) end
else
if self.t_score_lbl then tex.draw_texture(self.t_score_lbl, { index = p_idx }) end
end
if self.score ~= "" and self.t_score_num then
local n = math.floor(tonumber(self.score) or 0)
local str = tostring(n)
for i = 1, #str do
local d = tonumber(string.sub(str, #str - i + 1, #str - i + 1)) or 0
tex.draw_texture(self.t_score_num, {
frame = d,
x = -((i - 1) * self.sc_result_score_margin),
index = p_idx,
})
end
end
end
function ResultPlayer:draw_modifiers()
local p_idx = self.is_2p and 1 or 0
if self.mod_display and self.t_mod_doron then tex.draw_texture(self.t_mod_doron, { index = p_idx }) end
if self.mod_inverse and self.t_mod_abekobe then tex.draw_texture(self.t_mod_abekobe, { index = p_idx }) end
if self.mod_random == 1 and self.t_mod_kimagure then
tex.draw_texture(self.t_mod_kimagure, { index = p_idx })
elseif self.mod_random == 2 and self.t_mod_detarame then
tex.draw_texture(self.t_mod_detarame, { index = p_idx })
end
if self.mod_speed >= 40 and self.t_mod_yonbai then
tex.draw_texture(self.t_mod_yonbai, { index = p_idx })
elseif self.mod_speed >= 30 and self.t_mod_sanbai then
tex.draw_texture(self.t_mod_sanbai, { index = p_idx })
elseif self.mod_speed > 10 and self.t_mod_baisaku then
tex.draw_texture(self.t_mod_baisaku, { index = p_idx })
end
end