The original plays se_result `scorerank_NN_<seat>` (one sample for every rank)
when the plate appears; shipped as scorerank_{c,l,r} and played when the
ScoreRank state starts, seat chosen like the crown sound.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
795 lines
44 KiB
Lua
795 lines
44 KiB
Lua
-- YataiDON-HSS: Nijiiro arcade result board (lumen 000_default/result/result, script_lua/result/ResultMain.lua + ScoreBase.lua)
|
|
-- Layout (1080p px, 1P = left board; 2P = same board shifted +956 px = main.score_base_R vs score_base_L):
|
|
-- board frame main__shape313/317 @ (40,180)/(996,180) difficulty_mc tab @ score_base+(-596.6,-187.7)
|
|
-- option_icon_bg @ score_base+(-353,-167), option icons 43px pitch total score digits @ score_base+(-318,20) pitch 46
|
|
-- judge table: score_board_count_label @ score_base+(-90,93); 4-digit counters @ score_base+(249, -30/32/94/156/218) pitch 31
|
|
-- crown_mc @ score_base+(-212,165) best_score_mc @ score_base+(-316,-46) tamashii gauge (0.7 scale) @ main+(-466,-302)
|
|
-- donback @ main+(-464,244) msg_L_mc @ main+(-300,246) control_guide_L @ main+(-476,486)
|
|
-- Texture names are the parent's (the TexID enum is generated from PyTaikoGreen); see Graphics/result/MAPPING.md.
|
|
ResultPlayer = {}
|
|
ResultPlayer.__index = ResultPlayer
|
|
|
|
local GAUGE_SCALE = 0.7 -- arcade places tamashii_gage at 0.7 on the result board
|
|
local GAUGE_UNIT = 12 * GAUGE_SCALE -- 12 px per YataiDON gauge unit in the plate groove (87 units)
|
|
local OPTION_PITCH = 43
|
|
|
|
-- ROUND 15 — the whole result sequence is now the cabinet's own state machine, read out of
|
|
-- `script_lua/result/ResultMain.lua` (Main.m_State) and `ScoreBase.lua`. Every number below is
|
|
-- a frame count from those scripts; nothing here is tuned by eye. 1 frame = 1000/120 ms
|
|
-- (ROUND 24, r24-fpsaudit: Common.FPS is the cabinet's 120 fps script-logic clock, not 60 --
|
|
-- see ENGINE_BINDINGS.md; this file's ms comments below are corrected to match).
|
|
--
|
|
-- Fadein g_isFadefinish -> BGM primal_loop
|
|
-- WaitUpdateTamashiiGage timer_ > 100 (833.3 ms) then count_up_loop_c starts
|
|
-- UpdateTamashiiGage kGageSpeed_ = 7 frames per gauge unit, out of kMaxGage_ = 50
|
|
-- WaitUpdateScore timer_ > 100 (833.3 ms) then StartScoreDisplay
|
|
-- UpdateScore timer_ > 500 (4166.7 ms)
|
|
-- ScoreRank skipped outright when g_scoreRank == 0 -- YataiDON has no rank
|
|
-- Crown timer_ > 150 (1250.0 ms) then MoveResultAction
|
|
-- MoveResultAction MoveDonResult + MoveBg (StartSuccessEffect) + DisplayMessage
|
|
--
|
|
-- Two consequences the earlier rounds had wrong:
|
|
-- * the clear ("success") backdrop and the message bubble are NOT gauge/crown-settle events.
|
|
-- Both fire in MoveResultAction, at the END of the Crown state -- 10.8 s after the count-up
|
|
-- starts. We fired the gold backdrop the moment the gauge finished, roughly 12 s early.
|
|
-- * the gauge fills at 7 frames per arcade unit, i.e. 350 frames (2916.7 ms at 120 fps) for a
|
|
-- full bar. Our 50 ms per YataiDON unit x 87 units = 4350 ms, too slow at the corrected rate.
|
|
local F = 1000 / 120 -- ROUND 24 (r24-fpsaudit): Common.FPS is the cabinet's
|
|
-- 120 fps script-logic clock, not 60 -- see ENGINE_BINDINGS.md
|
|
local WAIT_GAUGE_MS = 100 * F -- 833.3 fade-in done -> the gauge starts filling
|
|
local GAUGE_MS_UNIT = 7 * F * (50 / 87) -- 33.53 ms per YataiDON unit == 7 f per arcade unit
|
|
local WAIT_SCORE_MS = 100 * F -- 833.3 gauge full -> StartScoreDisplay
|
|
local SCORE_TO_CROWN = 500 * F -- 4166.7 UpdateScore state length
|
|
local CROWN_TO_ACTION = 150 * F -- 1250.0 Crown state length -> MoveResultAction
|
|
-- ScoreRank state (between UpdateScore and Crown) when the score earned a rank: the
|
|
-- arcade result/result.nulm `score_rank_mc` (`start` f5 .. `end` f120 at 60 fps).
|
|
local RANK_STATE_MS = 120 * (1000 / 60) -- 2000.0
|
|
local ClipPlayer = require("game/clip_player")
|
|
-- score_rank_mc / score_rank_kiwami_mc placements inside the 1920x1080 result stage
|
|
local RANK_POS = { L = { 181.1, 500.7 }, R = { 1126.9, 501.0 } }
|
|
local KIWAMI_POS = { L = { 198.1, 505.7 }, R = { 1144.0, 506.0 } }
|
|
local RANK_SHAPES = { 74, 76, 78, 80, 82, 84, 86 } -- rank 1..7 plates inside score_rank
|
|
local RANK_THRESHOLDS = { 500000, 600000, 700000, 800000, 900000, 950000, 1000000 }
|
|
local function rank_of(score)
|
|
local r = 0
|
|
for i, t in ipairs(RANK_THRESHOLDS) do if score >= t then r = i end end
|
|
return r
|
|
end
|
|
|
|
-- ROUND 15 — the digit pop. Every arcade digit MC plays a scale keyframe when it lands on
|
|
-- `numN`; we drew the settled frame flat. Both curves are exported straight out of
|
|
-- result/result.nulm (ANIM_PIPELINE.md), so they are the cabinet's numbers, not a guess:
|
|
-- judge counters (sprite 167, digit4): scale 1.300 -> 1.000 LINEAR over 4 frames (66.7 ms)
|
|
-- score counters (sprite 245, digit7): a 19-frame squash-and-stretch --
|
|
-- 0.90 -> 1.40 (f0..6) -> 1.00 (f11) -> 0.90 (f15) -> 1.00 (f19), 316.7 ms
|
|
-- `center = true` scales about the digit cell's own centre, so a settled digit (scale 1)
|
|
-- lands on exactly the texture.json pixel it landed on before this change.
|
|
local _dg_ok, dg_anim = pcall(require, "anim/sampler")
|
|
if not _dg_ok then dg_anim = { safe = function()
|
|
local n = {}
|
|
n.at = function() return nil end; n.get = function() return nil end
|
|
n.label = function() return nil end; n.has = function() return false end
|
|
n.first = function() return 0 end; n.last = function() return 0 end
|
|
return n
|
|
end } end
|
|
local DGJ, DGJ_TRACK = dg_anim.safe("anim/result_digit_judge"), "#147@0"
|
|
local DGS, DGS_TRACK = dg_anim.safe("anim/result_digit_score"), "#225@0"
|
|
local DGJ_F0, DGJ_F1 = DGJ:first(), DGJ:last()
|
|
local DGS_F0, DGS_F1 = DGS:first(), DGS:last()
|
|
|
|
-- ROUND 53 (r53-anim-lumen-sweep): the success backdrop cross-fade, sampled from the
|
|
-- cabinet's own timeline. `Scripts/anim/result_bg_success.lua` is sprite 88 (`bg_1p_l`)
|
|
-- of `result/result_bg.nulm`: at the `success` label (f5) it places `crear_1P_l` at
|
|
-- alpha 0 and fades it to 1 over f5..f14 — 9 frames = 150 ms, not the 333 ms the
|
|
-- inherited PyTaikoGreen animation.json id 1 plays (R17 measured this ramp but only used
|
|
-- it to re-arm the Fuji clock; the fade itself stayed on the fan value). Fail-soft: with
|
|
-- the table missing, success_fade_alpha() returns nil and every caller falls back to the
|
|
-- engine animation's attribute (ROUND 8 rule).
|
|
local SBG = dg_anim.safe("anim/result_bg_success")
|
|
local SBG_F0 = SBG:label("success") or 5
|
|
local function success_fade_alpha(t0, now_ms)
|
|
if not t0 then return nil end
|
|
return SBG:get("crear_1P_l", SBG_F0 + (now_ms - t0) * 0.06, "a")
|
|
end
|
|
|
|
-- ROUND 53 (r53-anim-lumen-sweep): 魂 fire + rainbow gauge, per-frame from the cabinet's
|
|
-- own `enso_normal/enso/tamashiigage/tamashii_gage.nulm`.
|
|
-- * `anim/fever_fire` = sprite 58 (`fire` MC): fever_start f1..9 (alpha fade-in + growth),
|
|
-- fever loop f10..33 (8 flame cels x 3 frames = 400 ms), fever_end f34..44. The flame
|
|
-- cels are the display-list leaves #40/#44/../#56 -> texture frames 0..7.
|
|
-- * The disc blink (#35 bright / #42 plain at depth 1) and the sheen (#39 at depth 2,
|
|
-- alpha 0.6016 -- the old hand value was 0.5) are INTERMITTENT display-list presence,
|
|
-- which the exporter flattens into one continuous row span (known pipeline limitation,
|
|
-- recorded in ANIM_COVERAGE.md). The parity rule below -- bright disc + sheen on for
|
|
-- (frame - fever) % 4 < 2 -- was verified against the dense dump on every frame of the
|
|
-- f10..f33 loop.
|
|
-- * `anim/gauge_rainbow` = sprite 31 (41 f): each rainbow cel holds alpha 1 while the next
|
|
-- fades in linearly over 5 frames = 83.33 ms/step, 666.7 ms full cycle. The old hand
|
|
-- period was 75 ms.
|
|
-- The clip's fever_start GROWTH channel (sx 0.14 -> 0.80, bottom-anchored via its ty) is
|
|
-- in the table but deliberately NOT applied yet: draw_texture scales around the frame's
|
|
-- top-left here and the anchor compensation is unverified -- same class as best_score's
|
|
-- unapplied sy (C-res-4). Fail-soft throughout: with a table missing these return nil and
|
|
-- the callers fall back to the pre-R53 engine-animation ladder.
|
|
local FIRE = dg_anim.safe("anim/fever_fire")
|
|
local FIRE_LEAVES = { { "#40@0", 0 }, { "#44@0", 1 }, { "#46@0", 2 }, { "#48@0", 3 },
|
|
{ "#50@0", 4 }, { "#52@0", 5 }, { "#54@0", 6 }, { "#56@0", 7 } }
|
|
local FIRE_START = FIRE:label("fever_start") or 1
|
|
local FIRE_LOOP = FIRE:label("fever") or 10
|
|
local FIRE_END = FIRE:label("fever_end") or 34
|
|
local FIRE_SHEEN_A = FIRE:get("#39@2", (FIRE_LOOP + FIRE_END) / 2, "a") or 0.5
|
|
|
|
-- flame cel, alpha and sheen alpha at (now - t0) ms since the gauge maxed; nil = no table
|
|
local function fire_state(t0, now_ms)
|
|
if not t0 or t0 < 0 or not FIRE.tracks then return nil end
|
|
local f = FIRE_START + (now_ms - t0) * 0.06
|
|
if f >= FIRE_LOOP then
|
|
f = FIRE_LOOP + math.fmod(f - FIRE_LOOP, FIRE_END - FIRE_LOOP)
|
|
end
|
|
local fi = math.floor(f)
|
|
for i = 1, #FIRE_LEAVES do
|
|
local rows = FIRE.tracks[FIRE_LEAVES[i][1]]
|
|
if rows and #rows > 0 and fi >= rows[1][1] and fi <= rows[#rows][1] then
|
|
local a = FIRE:get(FIRE_LEAVES[i][1], f, "a")
|
|
if a then
|
|
local sheen
|
|
if fi < FIRE_LOOP then
|
|
sheen = FIRE:get("#39@2", f, "a") or 0.0 -- start ramp 0 -> 0.6016
|
|
else
|
|
sheen = ((fi - FIRE_LOOP) % 4 < 2) and FIRE_SHEEN_A or 0.0
|
|
end
|
|
return FIRE_LEAVES[i][2], a, sheen
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local RBW = dg_anim.safe("anim/gauge_rainbow")
|
|
local RBW_LEAVES = { { "#15@0", 0 }, { "#17@1", 1 }, { "#19@2", 2 }, { "#21@3", 3 },
|
|
{ "#23@4", 4 }, { "#25@5", 5 }, { "#27@6", 6 }, { "#29@7", 7 },
|
|
{ "#15@8", 0 } }
|
|
local RBW_LEN = RBW:last() - RBW:first() -- 40 frames = 666.7 ms cycle
|
|
|
|
local function pop_scale(clip, track, f0, f1, land_ms, now_ms)
|
|
if not land_ms then return 1.0 end
|
|
local f = f0 + (now_ms - land_ms) * 0.06
|
|
if f > f1 then return 1.0 end
|
|
return clip:get(track, f, "sx") or 1.0
|
|
end
|
|
|
|
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
|
|
|
|
-- ROUND 48 (r48-soulgauge-chn-port): the reference build norma semantics replace the
|
|
-- Green-era clear_start {52,60,69} table. The clear line is TamashiiNorm
|
|
-- (easy 6000 / normal 7000 / HARD 7000 / oni 8000 of the raw 0..10000
|
|
-- soul, corpus-constant per tier) — hard shares normal's norma and thus
|
|
-- the segment-35 ("_normal") clear-zone art, not Green's hard-at-79%
|
|
-- layout. gauge_length arrives as the 87-unit display value
|
|
-- (soul / 10000 * 87), so the raw soul is recovered for the test.
|
|
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]
|
|
self.norma = NORMA[d]
|
|
if gauge_length == 87 then
|
|
self.gauge_state = 2
|
|
elseif gauge_length / 87.0 * 10000.0 >= self.norma - 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.gauge_start_ms = -1
|
|
self.gauge_shown = 0 -- units currently filled (animates up to gauge_length)
|
|
self.gauge_done = false
|
|
self.score_delay = nil
|
|
self.fade_in_started = false -- read by C++: starts the score roll
|
|
self.rainbow_start_ms = -1.0
|
|
self.rainbow_frac = 0.0
|
|
self.crown_at = nil
|
|
self.msg_at = nil -- MoveResultAction: message bubble + success backdrop + Don pose
|
|
self.skipped = false
|
|
-- ROUND 19: read by C++ every frame (`ResultPlayer::reveal_end_ms`, ENGINE_BINDINGS
|
|
-- ROUND 19). 0/nil = the reveal is still running, so a drum press is a SKIP; once it
|
|
-- carries the MoveResultAction timestamp a press is an ADVANCE instead. That is the
|
|
-- cabinet's own boundary: MoveResultAction is the last thing ResultMain's `Crown`
|
|
-- state does, and every state after it (Medal / WaitEffectEnd / WaitNextScene)
|
|
-- reveals nothing and ignores isEffectSkip_.
|
|
self.reveal_end_ms = 0
|
|
-- ROUND 15: the engine's ResultPlayer reads these three fields once, right after the
|
|
-- script loads (src/objects/result/player.cpp). `count_up_instant` turns off the
|
|
-- per-digit ScoreAnimator roll: on the cabinet, ScoreBase.lua only ever calls
|
|
-- SkipCounter(), so every row LANDS on its final value -- no digit ever spins. The two
|
|
-- cadences are ScoreBase.lua's own `timer_ > 50` / `timer_ > 100`.
|
|
self.count_up_instant = true
|
|
self.count_up_row_ms = 50 * F
|
|
self.count_up_score_ms = 100 * F
|
|
-- ROUND 52 (r52-lua-divergence-fixes): the arcade count-up SE names, read
|
|
-- by the same engine block (result/player.cpp update_score_animation).
|
|
-- ScoreBase.lua:191 plays count_stop_c per landed row and :174/:198
|
|
-- don_big on the total score, BOTH gated `this.player_ == 1` (1P-only, in
|
|
-- duet too); the engine used to hardcode "num_up"/"don" for every seat.
|
|
self.count_up_row_sound = "count_stop"
|
|
self.count_up_score_sound = "don_big"
|
|
self.count_up_sound_1p_only = true
|
|
-- ROUND 52: seat suffixes for the arcade result cues.
|
|
-- count_up_loop / atmos_clear: _c solo, _l/_r in duet
|
|
-- (ResultMain.lua:459-464 / :1153-1162)
|
|
-- achieve_tamashii: _l for 1P even solo, _r for 2P
|
|
-- (ResultMain.lua:1086-1088: idx==1 && entryType 0|2 -> _l)
|
|
self.seat_c = has_2p and (is_2p and "_r" or "_l") or "_c"
|
|
self.seat_lr = is_2p and "_r" or "_l"
|
|
self.count_loop_on = false
|
|
self.achieve_played = false
|
|
self.skip_don_big_played = false
|
|
-- per-row landing times, for the arcade digit pop (see draw_score_info / draw_total_score)
|
|
self.row_land_ms = {}
|
|
self.score_land_ms = nil
|
|
|
|
self.crown_obj = nil
|
|
self.crown_msg_obj = nil
|
|
self.high_score_obj = nil
|
|
|
|
self.fade_in_bottom = tex.get_animation(1, "result") -- success bg fade -- ROUND 53:
|
|
-- fallback only; the drawn alpha is sampled from anim/result_bg_success (150 ms)
|
|
self.tamashii_fire = tex.get_animation(20, "result")
|
|
|
|
-- background success overlay (1P full / 2P halves)
|
|
self.t_success = tex.load_texture(has_2p and "result/background/footer_2p" or "result/background/footer_1p")
|
|
-- board
|
|
self.t_board = tex.load_texture("result/score/overlay") -- frames: 0 = left board, 1 = right board
|
|
self.t_difficulty = tex.load_texture("result/score/difficulty") -- 5 frames + course name
|
|
self.t_opt_bg = tex.load_texture("result/score/mod_sanbai") -- option pill, frame = icon count-1
|
|
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_speed = tex.load_texture("result/score/mod_baisaku") -- 14 frames x1.1 .. x4
|
|
self.t_judge_labels = tex.load_texture("result/score/max_combo") -- whole 良/可/不可/連打数/最大コンボ数 block (ja/en)
|
|
self.t_judge_num = tex.load_texture("result/score/judge_num")
|
|
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_donback = tex.load_texture("result/bottom/chara_0") -- frames: 0 = 1P red, 1 = 2P blue
|
|
-- NO control guide: the arcade's control_guide_L/R (common/control_guide_xs) is created by
|
|
-- ResultReward:InitControlGuide with SetVisible(false) and is only turned on inside the
|
|
-- reward-choice phase (StateId.RewardChoicePanel_, SetupMenFuchiKR("select","decide")), then
|
|
-- FadeOut() once the choice is made. A plain result screen -- which is all YataiDON has --
|
|
-- never shows it. `result/bottom/chara_center` is an 8x8 blank. See MAPPING.md ROUND 6.
|
|
|
|
-- gauge (HUD bakes, drawn at 0.7)
|
|
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_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")
|
|
|
|
-- digit pitches are fixed by the arcade digit MCs (digit4 pitch 31, digit7 pitch 46), not skin_config
|
|
self.sc_score_info_margin = 31
|
|
self.sc_result_score_margin = 46
|
|
|
|
-- option icon list (arcade order: speed, doron, abekobe, random)
|
|
self.options = {}
|
|
if mod_speed > 10 then
|
|
local speeds = { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30, 35, 40 }
|
|
local best, bi = 1e9, 1
|
|
for i, s in ipairs(speeds) do
|
|
if math.abs(s - mod_speed) < best then best, bi = math.abs(s - mod_speed), i end
|
|
end
|
|
table.insert(self.options, { self.t_mod_speed, bi - 1 })
|
|
end
|
|
if mod_display then table.insert(self.options, { self.t_mod_doron, 0 }) end
|
|
if mod_inverse then table.insert(self.options, { self.t_mod_abekobe, 0 }) end
|
|
if mod_random == 1 then table.insert(self.options, { self.t_mod_kimagure, 0 })
|
|
elseif mod_random == 2 then table.insert(self.options, { self.t_mod_detarame, 0 }) end
|
|
|
|
return self
|
|
end
|
|
|
|
function ResultPlayer:update(current_ms, fade_in_finished,
|
|
score, good, ok, bad, max_combo, total_drumroll, is_skipped)
|
|
self.fade_in_finished_flag = fade_in_finished
|
|
self.now_ms = current_ms
|
|
-- ROUND 15: record when each row first shows a value, so the digits can play the
|
|
-- cabinet's pop. With `count_up_instant` a row goes "" -> final in one frame, which
|
|
-- is exactly the cabinet's SkipCounter().
|
|
local prev = { self.good, self.ok, self.bad, self.total_drumroll, self.max_combo }
|
|
self.score, self.good, self.ok, self.bad = score, good, ok, bad
|
|
self.max_combo, self.total_drumroll = max_combo, total_drumroll
|
|
local now_rows = { good, ok, bad, total_drumroll, max_combo }
|
|
for i = 1, 5 do
|
|
if prev[i] == "" and now_rows[i] ~= "" and not self.row_land_ms[i] then
|
|
self.row_land_ms[i] = current_ms
|
|
end
|
|
end
|
|
if self.score ~= "" and not self.score_land_ms then self.score_land_ms = current_ms end
|
|
|
|
-- isEffectSkip_ (ResultMain): every remaining state collapses at once.
|
|
if is_skipped and not self.skipped then
|
|
self.skipped = true
|
|
self.gauge_shown = math.floor(self.gauge_length)
|
|
-- ROUND 52: skip cuts the count-up loop (UpdateTamashiiGage's
|
|
-- isEffectSkip branch does TSound:StopSlot, ResultMain.lua:1069) and
|
|
-- lands everything on one don_big (ScoreBase UpdateScoreDisplay's
|
|
-- skip branch, :174 -- 1P-only; the per-row count_stop_c never fires).
|
|
if self.count_loop_on then
|
|
self.count_loop_on = false
|
|
if audio.stop_sound then audio.stop_sound("count_up_loop" .. self.seat_c) end
|
|
end
|
|
if self.player_num == 1 and not self.skip_don_big_played then
|
|
self.skip_don_big_played = true
|
|
audio.play_sound("don_big", "sound")
|
|
end
|
|
end
|
|
|
|
if fade_in_finished and not self.gauge_started then
|
|
self.gauge_started = true
|
|
self.gauge_start_ms = current_ms
|
|
self.tamashii_fire:start()
|
|
end
|
|
|
|
if self.gauge_started then
|
|
self.tamashii_fire:update(current_ms)
|
|
local target = math.floor(self.gauge_length)
|
|
-- WaitUpdateTamashiiGage: 100 frames of nothing, then 7 frames per arcade unit
|
|
local elapsed = current_ms - self.gauge_start_ms - WAIT_GAUGE_MS
|
|
if self.skipped then
|
|
self.gauge_shown = target
|
|
elseif elapsed > 0 then
|
|
-- ROUND 52: WaitUpdateTamashiiGage ends here -- start the arcade
|
|
-- count-up loop (ResultMain.lua:459-464, PlaySlot count_up_loop_*;
|
|
-- looped by the new engine binding, cut below when the fill lands).
|
|
if not self.count_loop_on and not self.gauge_done then
|
|
self.count_loop_on = true
|
|
audio.play_sound("count_up_loop" .. self.seat_c, "sound", true)
|
|
end
|
|
self.gauge_shown = math.min(target, math.floor(elapsed / GAUGE_MS_UNIT))
|
|
-- ROUND 52: the fill reaching the rainbow top plays
|
|
-- achieve_tamashii (ResultMain.lua:1086-1088; non-skip path only).
|
|
if not self.achieve_played and self.gauge_state == 2 and self.gauge_shown >= 87 then
|
|
self.achieve_played = true
|
|
audio.play_sound("achieve_tamashii" .. self.seat_lr, "sound")
|
|
end
|
|
end
|
|
-- ROUND 49 (r49-lua-chn-crosscheck): completion may only be declared after the
|
|
-- WaitUpdateTamashiiGage dwell has actually elapsed (ResultMain.lua:457 `timer_ > 100`
|
|
-- runs UNCONDITIONALLY, even when the gauge has nothing to fill). Without the
|
|
-- `elapsed > 0` arm, a total-miss run (gauge_length == 0) satisfied
|
|
-- `gauge_shown >= target` on the very first gauge frame and armed score_delay
|
|
-- 833 ms early, skipping the state the cabinet always sits through.
|
|
if self.gauge_shown >= target and not self.gauge_done
|
|
and (self.skipped or elapsed > 0) then
|
|
self.gauge_done = true
|
|
self.score_delay = current_ms + (self.skipped and 0 or WAIT_SCORE_MS)
|
|
-- ROUND 52: StopSlot the moment the fill lands on its target
|
|
-- (ResultMain.lua:1094 `current == g_tamashiiGage` -> StopSlot).
|
|
if self.count_loop_on then
|
|
self.count_loop_on = false
|
|
if audio.stop_sound then audio.stop_sound("count_up_loop" .. self.seat_c) end
|
|
end
|
|
end
|
|
if self.gauge_state == 2 and self.gauge_shown >= 87 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
|
|
end
|
|
self.fade_in_bottom:update(current_ms)
|
|
-- ROUND 11: publish the success cross-fade so ResultBackground / ResultFadeIn can swap the
|
|
-- live backdrop layers (gold sky, cyan pulsing Fuji, clear cloud bars) under the fireworks
|
|
-- overlay this same animation fades in. 1P only -- the arcade 2P backdrop has no clouds.
|
|
if not self.has_2p then
|
|
-- ROUND 53: sampled cabinet ramp (150 ms, f5..f14) wins; engine anim 1 (333 ms) is
|
|
-- the fail-soft fallback.
|
|
local sfa = success_fade_alpha(self.success_fade_t0, current_ms)
|
|
or self.fade_in_bottom.attribute
|
|
__hss_result_bg_clear = (self.gauge_state >= 1) and sfa or 0.0
|
|
end
|
|
|
|
if self.score_delay ~= nil and current_ms > self.score_delay and not self.fade_in_started then
|
|
self.fade_in_started = true
|
|
-- UpdateScore (500 f). ScoreRank is skipped outright -- YataiDON has no 粋/雅/極 rank,
|
|
-- and ResultMain skips that whole state when g_scoreRank == 0. Then the Crown state:
|
|
-- the crown flies in at its start, MoveResultAction fires 150 f later. With no crown
|
|
-- (a failed run) the Crown state is skipped too, so MoveResultAction is immediate.
|
|
self.crown_at = current_ms + SCORE_TO_CROWN
|
|
self.msg_at = self.crown_at + ((self.gauge_state >= 1) and CROWN_TO_ACTION or 0)
|
|
end
|
|
if self.skipped and self.fade_in_started then
|
|
self.rank_obj = nil
|
|
self.crown_at = math.min(self.crown_at or current_ms, current_ms)
|
|
self.msg_at = math.min(self.msg_at or current_ms, current_ms)
|
|
end
|
|
|
|
-- ScoreRank state (arcade: UpdateScore -> ScoreRank -> Crown), entered when the
|
|
-- score would otherwise hand over to the crown: the total is only known by then.
|
|
-- Skipped at rank 0 and on a skipped run, as the arcade skips the state.
|
|
if self.crown_at and current_ms >= self.crown_at and not self.rank_done then
|
|
self.rank_done = true
|
|
local rank = (not self.skipped) and rank_of(math.floor(tonumber(self.score) or 0)) or 0
|
|
if rank > 0 then
|
|
local side = self.is_2p and "R" or "L"
|
|
local pos = (rank == 7) and KIWAMI_POS[side] or RANK_POS[side]
|
|
local clip = (rank == 7) and "rank_result_kiwami" or "rank_result"
|
|
self.rank_obj = ClipPlayer.new("game/ending_anim/" .. clip, "game", clip .. "_anim", pos[1], pos[2])
|
|
if self.rank_obj then
|
|
self.rank_obj.hold_last = true
|
|
if rank < 7 then self.rank_obj.shape_map = { [RANK_SHAPES[1]] = RANK_SHAPES[rank] } end
|
|
self.rank_obj:start(current_ms)
|
|
-- se_result `scorerank_NN_<seat>`: one sample for every rank, seat like the crown
|
|
audio.play_sound("scorerank" .. (self.has_2p and (self.is_2p and "_r" or "_l") or "_c"), "sound")
|
|
self.crown_at = current_ms + RANK_STATE_MS
|
|
if self.msg_at then self.msg_at = self.msg_at + RANK_STATE_MS end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Crown state entry: StartCrownDisplay (crownMc_ GotoAndPlay "start" + crown_<colour>_c)
|
|
if self.crown_at and current_ms >= self.crown_at
|
|
and self.gauge_state >= 1 and not self.crown_obj then
|
|
-- ROUND 52: has_2p passed through so the crown SE picks its arcade
|
|
-- seat suffix (_c solo, _l/_r duet -- ScoreBase.lua:322-342).
|
|
self.crown_obj = ResultCrown.new(self.crown_type, self.is_2p, self.has_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
|
|
end
|
|
if self.crown_obj then self.crown_obj:update(current_ms) end
|
|
|
|
-- MoveResultAction: DisplayMessage + MoveBg(StartSuccessEffect) on the same beat.
|
|
-- The arcade message state is chosen from the SOUL GAUGE, not the crown:
|
|
-- perfect = gauge at max, success = cleared, near = failed but gauge > 30 % of max,
|
|
-- miss = everything else (ResultMain.DisplayMessage).
|
|
if self.msg_at and current_ms >= self.msg_at and not self.crown_msg_obj then
|
|
local frame
|
|
if self.gauge_state == 2 then frame = 3 -- perfect
|
|
elseif self.gauge_state >= 1 then frame = 2 -- success
|
|
elseif self.gauge_length > 87 * 0.3 then frame = 1 -- near
|
|
else frame = 0 end -- miss
|
|
self.crown_msg_obj = ResultCrownMessage.new(frame, self.is_2p)
|
|
local voices = { [0] = "max_fail_voice_", [1] = "fail_voice_",
|
|
[2] = "clear_voice_", [3] = "max_clear_voice_" }
|
|
audio.play_sound(voices[frame] .. tostring(self.player_num) .. "p", "voice")
|
|
if self.gauge_state >= 1 then
|
|
self.fade_in_bottom:start() -- MoveBg: success background
|
|
-- ROUND 53: arm the sampled f5..f14 cross-fade on the same beat
|
|
self.success_fade_t0 = current_ms
|
|
-- ROUND 52: the cleared-run ambience rides the same
|
|
-- MoveResultAction beat as the glad voice (ResultMain.lua
|
|
-- MoveDonResult :1153-1162 -- atmos_clear_c solo, _l/_r duet;
|
|
-- a failed run plays only the sad voice, no atmos).
|
|
audio.play_sound("atmos_clear" .. self.seat_c, "sound")
|
|
end
|
|
end
|
|
if self.crown_msg_obj then self.crown_msg_obj:update(current_ms) end
|
|
|
|
-- ROUND 19: MoveResultAction has fired -> the reveal is over. Publish the beat
|
|
-- itself (not `current_ms`), so the two cabinet dwells C++ adds on top of it are
|
|
-- measured from the same instant the arcade measures them from.
|
|
if self.reveal_end_ms == 0 and self.msg_at and current_ms >= self.msg_at then
|
|
self.reveal_end_ms = self.msg_at
|
|
end
|
|
|
|
-- DisplayHiscoreEffect runs inside the same branch that lands the total score.
|
|
if not self.high_score_obj and self.score_diff > 0 and self.score ~= "" 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
|
|
|
|
-- success background (ResultBg "success") — ROUND 53: sampled f5..f14 ramp, engine
|
|
-- anim 1 as fail-soft fallback (see success_fade_alpha above).
|
|
local success_a = success_fade_alpha(self.success_fade_t0, tex.get_current_ms())
|
|
or self.fade_in_bottom.attribute
|
|
if self.gauge_state >= 1 and success_a > 0 and self.t_success then
|
|
tex.draw_texture(self.t_success, { frame = self.has_2p and p_idx or 0, index = p_idx, fade = success_a })
|
|
end
|
|
|
|
-- board frame (opaque white body), then the red/blue donback panel on top of it, then the difficulty tab
|
|
if self.t_board then tex.draw_texture(self.t_board, { frame = p_idx, index = p_idx }) end
|
|
if self.t_donback then tex.draw_texture(self.t_donback, { frame = p_idx, index = p_idx }) end
|
|
if self.t_difficulty then tex.draw_texture(self.t_difficulty, { frame = self.selected_difficulty, index = p_idx }) end
|
|
|
|
-- option icons
|
|
if #self.options > 0 then
|
|
if self.t_opt_bg then tex.draw_texture(self.t_opt_bg, { frame = #self.options - 1, index = p_idx }) end
|
|
for i, o in ipairs(self.options) do
|
|
if o[1] then tex.draw_texture(o[1], { frame = o[2], x = (i - 1) * OPTION_PITCH, index = p_idx }) end
|
|
end
|
|
end
|
|
|
|
-- judge table labels
|
|
if self.t_judge_labels then tex.draw_texture(self.t_judge_labels, { index = p_idx }) end
|
|
|
|
self:draw_score_info()
|
|
self:draw_total_score()
|
|
|
|
if self.high_score_obj then self.high_score_obj:draw() end
|
|
if self.rank_obj then self.rank_obj:draw(tex.get_current_ms()) end
|
|
if self.crown_obj then self.crown_obj:draw() end
|
|
if self.crown_msg_obj then self.crown_msg_obj:draw() end
|
|
|
|
end
|
|
|
|
-- ROUND 9 (engine binding `ResultPlayer:chara_pos` -> {x, y, scale}, see
|
|
-- ENGINE_BINDINGS.md). Two arcade facts the engine used to make unreachable:
|
|
-- * the result Don is placed at scale 1.12 (don1p placeholder (237,773));
|
|
-- Chara3D's own scale was a fixed 650 with no multiplier.
|
|
-- * don_R sits at (1682,787), the mirror of don_L across the screen centre.
|
|
-- ResultPlayer::draw only ever added screen_height/2 for 2P, which put the
|
|
-- 2P Don 540 px below the 1P one - off the bottom of the screen.
|
|
-- Chara3D's visible centre is (x,y) + (5,-130)*scale and the feet land another
|
|
-- 125*scale below it, so holding the round-6 tuned centre (210, 805) / feet 935
|
|
-- at scale 1.12 gives (204, 941).
|
|
local CHARA_SCALE = 1.12
|
|
local CHARA_X = 204
|
|
local CHARA_Y = 941
|
|
local CHARA_X_2P = 1649 -- arcade don_R 1682 minus the same -27 the 1P value carries
|
|
|
|
function ResultPlayer:chara_pos()
|
|
return { self.is_2p and CHARA_X_2P or CHARA_X, CHARA_Y, CHARA_SCALE }
|
|
end
|
|
|
|
-- ROUND 17 (engine binding `ResultPlayer:nameplate_pos` -> {x, y, fade}, see
|
|
-- ENGINE_BINDINGS.md), the exact mirror of `chara_pos` above.
|
|
--
|
|
-- `result.nulm --sprite 319 --range 25,25` (`duet`) measured this round:
|
|
-- name_plate_L tx -754.0 ty +430 alpha 1
|
|
-- name_plate_R tx +753.2 ty +430 alpha 1
|
|
-- and at `use_left` (frame 5) only L is visible, at `use_right` (frame 15) only R
|
|
-- is -- so the cabinet's rule is SEAT-fixed: left board = left plate, right board
|
|
-- = right plate, both in duet. `Result2PScreen` builds player_1 on the left seat
|
|
-- and player_2 on the right, so this needs no join-order logic at all.
|
|
--
|
|
-- screen = (960,540) + (tx,ty) => L (206,970) R (1713.2,970)
|
|
-- our plate canvas is 408x96 with the MC-280 registration at canvas (204,48), so
|
|
-- the draw origin is centre - (204,48):
|
|
-- L (206-204, 970-48) = (2, 922) == skin_config `result_nameplate`
|
|
-- R (1713.2-204, 922) = (1509.2, 922) -> 1509, whole-pixel like every other
|
|
-- value in this skin (the plate art and its text are drawn 1:1, so a .2
|
|
-- destination would blur both -- ROUND 11 text-sharpness note).
|
|
local NAMEPLATE_X = 2
|
|
local NAMEPLATE_Y = 922
|
|
local NAMEPLATE_X_2P = 1509
|
|
|
|
function ResultPlayer:nameplate_pos()
|
|
return { self.is_2p and NAMEPLATE_X_2P or NAMEPLATE_X, NAMEPLATE_Y }
|
|
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 = GAUGE_SCALE
|
|
-- ROUND 26 (r26-resultgaugebox): the gauge's own box/frame used to cross-fade in over
|
|
-- animation 17 (366 ms) starting only once WaitUpdateTamashiiGage begins, so it visibly
|
|
-- lagged behind the score board -- which pops to full opacity the instant Fadein's wipe
|
|
-- clears, with no fade of its own. Nothing in ResultMain.lua/ScoreBase.lua's documented
|
|
-- state machine (see the ROUND 15 comment above) puts a box-opacity ramp anywhere in
|
|
-- Fadein -> WaitUpdateTamashiiGage; the only delay that state actually has is on the FILL
|
|
-- (gauge_shown / GAUGE_MS_UNIT below). This animation was inherited unaudited from
|
|
-- PyTaikoGreen (whose own player.lua still runs it) and has no cabinet counterpart, so the
|
|
-- box itself now snaps to full opacity together with the rest of the board.
|
|
local base_fade = 1.0
|
|
local gl_int = self.gauge_shown
|
|
-- ROUND 48: the clear-zone position follows the norma art tier
|
|
-- (hard = normal's segment-35 zone), see the init block above.
|
|
local clear_pts = { 52, 60, 69 }
|
|
local clear_pt = clear_pts[self.art_index + 1]
|
|
local u = GAUGE_UNIT
|
|
|
|
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 and gl_int >= 87 then
|
|
-- ROUND 53: rainbow cycle sampled from anim/gauge_rainbow (sprite 31): 83.33 ms per
|
|
-- step, base cel at 1 with the next fading in over it — the cabinet's own display
|
|
-- list, drawn leaf by leaf in depth order. Fallback = pre-R53 75 ms hand cycle.
|
|
local drew_rainbow = false
|
|
if self.t_gauge_rainbow and RBW.tracks and RBW_LEN > 0 and self.rainbow_start_ms >= 0 then
|
|
local f = math.fmod((tex.get_current_ms() - self.rainbow_start_ms) * 0.06, RBW_LEN)
|
|
local fi = math.floor(f)
|
|
for i = 1, #RBW_LEAVES do
|
|
local rows = RBW.tracks[RBW_LEAVES[i][1]]
|
|
if rows and #rows > 0 and fi >= rows[1][1] and fi <= rows[#rows][1] then
|
|
local a = RBW:get(RBW_LEAVES[i][1], f, "a") or 0.0
|
|
if a > 0.0 then
|
|
tex.draw_texture(self.t_gauge_rainbow,
|
|
{ frame = RBW_LEAVES[i][2], scale = scale, fade = base_fade * a, index = p_idx })
|
|
drew_rainbow = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if not drew_rainbow and self.t_gauge_rainbow then
|
|
local fa = math.floor(self.rainbow_frac) % 8
|
|
local fb = (fa + 1) % 8
|
|
local t = self.rainbow_frac - math.floor(self.rainbow_frac)
|
|
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 gl_int > 0 and self.t_gauge_bar then
|
|
local n = math.min(gl_int, clear_pt - 1)
|
|
tex.draw_texture(self.t_gauge_bar, { scale = scale, x2 = (n - 1) * u, fade = base_fade, index = p_idx })
|
|
end
|
|
if gl_int >= clear_pt - 1 and self.t_gauge_bar_clear_transition then
|
|
tex.draw_texture(self.t_gauge_bar_clear_transition,
|
|
{ scale = scale, x = (clear_pt - 1) * u, fade = base_fade, index = p_idx })
|
|
end
|
|
if gl_int > clear_pt then
|
|
local extra = (gl_int - clear_pt - 1) * u
|
|
if self.t_gauge_bar_clear_top then
|
|
tex.draw_texture(self.t_gauge_bar_clear_top,
|
|
{ scale = scale, x = clear_pt * u, 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 * u, 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
|
|
|
|
local clear_idx = self.art_index + p_idx * 3
|
|
-- ROUND 48: a failed run never lights the クリア caption, even when the
|
|
-- final fill pixel-touches the clear zone (the reference build lights it on
|
|
-- IsNormaClear, i.e. gauge_state >= 1).
|
|
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
|
|
-- ROUND 53: flame cel + fade-in and the sheen duty cycle now come from
|
|
-- anim/fever_fire (sprite 58); see the fire_state block up top. ROUND 17's anchor
|
|
-- findings stand: the flame holds local (+43,-43) scale **0.800** through the
|
|
-- `fever` loop, which is why the drawn scale stays a constant 0.8 here.
|
|
local fire_sheen = nil
|
|
if self.gauge_state == 2 and gl_int >= 87 and self.t_gauge_tamashii_fire then
|
|
local ff, fa2, sheen_a = fire_state(self.rainbow_start_ms, tex.get_current_ms())
|
|
if ff then
|
|
fire_sheen = sheen_a
|
|
tex.draw_texture(self.t_gauge_tamashii_fire,
|
|
{ frame = ff, scale = 0.8 * scale, fade = fa2 * base_fade, index = p_idx })
|
|
else
|
|
-- fail-soft: pre-R53 engine ladder (animation 20, 8 x 50 ms)
|
|
local fire_f = math.floor(self.tamashii_fire.attribute)
|
|
tex.draw_texture(self.t_gauge_tamashii_fire,
|
|
{ frame = fire_f, scale = 0.8 * scale, fade = base_fade, index = p_idx })
|
|
end
|
|
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 gl_int >= 87 and self.t_gauge_tamashii_overlay then
|
|
if fire_sheen ~= nil then
|
|
if fire_sheen > 0.0 then
|
|
tex.draw_texture(self.t_gauge_tamashii_overlay,
|
|
{ scale = scale, fade = math.min(fire_sheen, base_fade), index = p_idx })
|
|
end
|
|
else
|
|
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
|
|
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()
|
|
-- Index order == the arcade's own ScoreBase.lua numMc_[1..5]:
|
|
-- good 良 (y 314), ok 可 (376), bad 不可 (438), dramroll 連打数 (500), maxcombo 最大コンボ数 (562)
|
|
-- measured off result/result.nulm `score_base_L` at `use_left` (good_num_mc … max_combo_num_mc).
|
|
-- ROUND 15 bug fix: `result/score/texture.json` had judge_num[3].y = 538 and [4].y = 476,
|
|
-- i.e. slot 3 was the BOTTOM (最大コンボ数) row -- so since round 5, when this array was
|
|
-- reordered to fill top-down, the drumroll count was printed on the 最大コンボ数 line and
|
|
-- the max combo on the 連打数 line. texture.json now runs 290/352/414/476/538 (and
|
|
-- 1798 + the same five for 2P), which is top-down, matching this array and the label block.
|
|
local scores = { self.good, self.ok, self.bad, self.total_drumroll, self.max_combo }
|
|
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 pop = pop_scale(DGJ, DGJ_TRACK, DGJ_F0, DGJ_F1,
|
|
self.row_land_ms[j], self.now_ms or 0)
|
|
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,
|
|
scale = pop,
|
|
center = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ResultPlayer:draw_total_score()
|
|
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 not self.fade_in_finished_flag then return end
|
|
if self.score ~= "" and self.t_score_num then
|
|
local pop = pop_scale(DGS, DGS_TRACK, DGS_F0, DGS_F1,
|
|
self.score_land_ms, self.now_ms or 0)
|
|
local n = math.floor(tonumber(self.score) or 0)
|
|
local str = tostring(n)
|
|
-- arcade draws total_score_back_mc (all outlines) first, then total_score_mc (all fills) on top:
|
|
-- score_num frames 0..9 = outline layer, 10..19 = fill layer (see MAPPING.md ROUND 5)
|
|
for layer = 0, 10, 10 do
|
|
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 + layer,
|
|
x = -((i - 1) * self.sc_result_score_margin),
|
|
index = p_idx,
|
|
scale = pop,
|
|
center = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|