-- 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_`: 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__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