ResultPlayer = {} ResultPlayer.__index = ResultPlayer local VM = require("result.swg_vm") local GAME = "t8" local floor, min, max = math.floor, math.min, math.max local K = tex.get_screen_width() / 640 local FPS = 60 -- engine ResultScreen (scenes/result.h): exit accepted from reveal_end + these local K_WAIT_EFFECT, K_WAIT_NEXT = 500 * 1000 / 120, 500 * 1000 / 120 local NORMA = { [0] = 6000, [1] = 7000, [2] = 7000, [3] = 8000, [4] = 8000 } local SS = tex.get_screen_scale() -- the song title / subtitle where YataiDON14 puts them (dispconf [成績発表]: centred at (320, 130) and -- (320, 170), 32 / 26 px, white with a 4.5 px black ring; wider than the screen less 12 px a side: -- squeezed) local TITLE = { x = 320, y = 130, size = 32 } local SUBTITLE = { x = 320, y = 170, size = 26 } local TITLE_SQUEEZE = 0.8 -- shared by the two panels of a 2P result: the second panel only hands its numbers over T12_RESULT = T12_RESULT or {} local DATA local TEX, SIZE = {}, {} local function texture(i) local t = TEX[i] if t == nil then t = tex.get_texture(string.format("result/%s_result/t%02d", GAME, i)) or false TEX[i] = t end return t or nil end local ADDITIVE = { [1] = true, [2] = true } if GAME == "t8" then ADDITIVE = { [0] = true, [1] = true, [2] = true } end local TEX_W = {} local function texture_w(i) -- white copy for the colour offset pass local t = TEX_W[i] if t == nil then t = tex.get_texture(string.format("result/%s_result/t%02d_w", GAME, i)) or false TEX_W[i] = t end return t or texture(i) end local function num(s) return floor(tonumber(s) or 0) end -- only a drum face (don / F J) skips the movie: any key (space ...) did too (user 2026-10-03) local function LAST_DON() if tex.last_don_input_ms then return tex.last_don_input_ms() end return tex.last_input_ms() end local function snd_key(name) return GAME .. "_" .. string.lower(name) end local function snd_kind(name) if name:sub(1, 4) == "STR_" then return "music" end if name:sub(1, 3) == "VO_" then return "voice" end return "sound" 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_folder("result", GAME .. "_result") TEX, TEX_W = {}, {} if not DATA then DATA = require("result." .. GAME .. ".result_swg") end for i, s in pairs(DATA.tex) do SIZE[i] = s end self.player_num, self.is_2p, self.has_2p = player_num, is_2p, has_2p local diff = max(0, min(4, selected_difficulty or difficulty or 0)) self.course = min(3, diff) self.gauge_pct = min(100, gauge_length / 87.0 * 100.0) self.cleared = gauge_length / 87.0 * 10000.0 >= NORMA[diff] - 0.5 self.gauge_mark = NORMA[diff] / 100 -- the clear line, in the gauge's 0..100 self.full = gauge_length >= 87 -- which movie player slot this panel is (0 = 1P, 1 = 2P) self.slot = (is_2p or (player_num == 2 and not has_2p)) and 1 or 0 self.owner = not is_2p -- this panel plays the movie if self.owner then T12_RESULT = { vals = {} } end self.score = "" self.t0, self.frames = nil, 0 self.fade_in_started = true -- the engine hands the numbers over at once self.count_up_instant = true self.count_up_row_ms, self.count_up_score_ms = 1, 1 self.count_up_row_sound, self.count_up_score_sound = "", "" self.reveal_end_ms = 1.0e300 self.last_input = LAST_DON() return self end function ResultPlayer:values(score, good, ok, bad, max_combo, total_drumroll) local g, o, b = num(good), num(ok), num(bad) local total = g + o + b return { getResulScoreValue = num(score), getResulComboValue = num(max_combo), getResulHitRateValue = total > 0 and floor((g + o) * 100 / total) or 0, getResultRyoNum = g, getResultKaNum = o, getResultFukaNum = b, getResultTotalRendaNum = num(total_drumroll), getResulGaugeRate = floor(self.gauge_pct), getResulSoulFire = self.full and 1 or 0, getResulSuccessOrFailure = self.cleared and 1 or 0, getResulCourseNo = self.course, getResultEndStatus = 0, getResultRankInStatus = 0, fc = total > 0 and b == 0, } end function ResultPlayer:start_movie() local vals = T12_RESULT.vals local label = "result_1P" if self.has_2p then label = "result_1P2P" elseif self.slot == 1 then label = "result_2P" end local muted = false local host = { call = function(name, args) local slot = floor(tonumber(args[1]) or 0) local v = vals[slot] or vals[self.slot] or {} if v[name] ~= nil then return v[name] end if name == "getEnsoGameNumber" then return 1 end return 0 end, sound = function(name) if not muted then audio.play_sound(snd_key(name), snd_kind(name)) end end, stop_sound = function(name) audio.stop_sound(snd_key(name)) end, on_new = function(inst) if inst.name == "main_movie" then local function A(a, b) local r = VM.new_array(2); r[0], r[1] = a, b; return r end inst.vars.success_flag = A(nil, nil) inst.vars.end_state = A(nil, nil) -- full combo per slot (0 = 1P, 1 = 2P): the movie only reads it (crown, the -- flower chara); no 不可 = full combo (user 2026-10-03: no crown was ever shown) local function fc(v) return v ~= nil and v.fc == true end inst.vars.full_combo = A(fc(vals[0]), fc(vals[1])) inst.vars.rank_in = A(false, false) -- the clear line (percent): the gauge fills up to it in red, the rest in the -- rainbow, and its 及格 mark sits there (sprites 213 / 216 / 218) inst.vars.gaugeMark = self.gauge_mark if label ~= "result_1P" then inst:go(label, false) end end end, } self.host = host self.set_muted = function(m) muted = m end self.movie = VM.new(DATA, host) end -- the movie's frame counter follows the screen clock at the movie's 60 fps function ResultPlayer:advance_to(n) while self.frames < n do if self.frames > 0 then self.movie:step() end self.movie:tick() self.frames = self.frames + 1 end end function ResultPlayer:board() if not self.board_inst then self.board_inst = self.movie:find(self.slot == 1 and not self.has_2p and "resultBoard_2P" or "resultBoard_1P") end return self.board_inst end function ResultPlayer:update(current_ms, fade_in_finished, score, good, ok, bad, max_combo, total_drumroll, is_skipped) self.now = current_ms if not self.t0 then self.t0 = current_ms audio.stop_sound("bgm") -- the engine's result bgm: the movie plays its own end if score == nil or score == "" then return end self.score = score T12_RESULT.vals = T12_RESULT.vals or {} T12_RESULT.vals[self.slot] = self:values(score, good, ok, bad, max_combo, total_drumroll) if GAME == "t8" and not self.progress_done then self.progress_done = true T8_PROGRESS = T8_PROGRESS or {} local p = T8_PROGRESS[self.slot + 1] or { scores = {} } p.scores[#p.scores + 1] = num(score) T8_PROGRESS[self.slot + 1] = p end if not self.owner then return end if not self.movie then if self.has_2p and not T12_RESULT.vals[1] then return end self:start_movie() self.m0 = current_ms end local due = floor((current_ms - self.m0) * FPS / 1000) + 1 -- a press before the board is done fast-forwards it (silently) to its skip check local li = LAST_DON() local b = self:board() local done_f = b and b.sp.labels["skip_check"] or 760 if li > self.last_input and b then -- (a press before the board exists waits for it) self.last_input = li if b and b.frame < done_f and self.reveal_end_ms > 1.0e299 then self.set_muted(true) local guard = 0 while b.frame < done_f and guard < 2000 do self:advance_to(self.frames + 1); guard = guard + 1 end self.set_muted(false) self.m0 = current_ms - (self.frames - 1) * 1000 / FPS due = self.frames -- the press that skipped must not also leave (user 2026-10-01/02: the first F/J went -- straight back to song select). The engine reads keys only once skip is enabled (fade-in -- + 100 frames) and a press made before that stays queued until then, so a short delay -- is not enough: leaving stays closed (the queued press is spent harmlessly) until a NEW -- press; with none, the engine's own timeout still leaves (reveal_end = now) self.reveal_end_ms = current_ms self.skip_press = li elseif self.skip_press and li > self.skip_press and b and b.frame >= done_f then self.reveal_end_ms = current_ms - (K_WAIT_EFFECT + K_WAIT_NEXT) - 1 self.skip_press = nil end end self:advance_to(due) b = self:board() if b and b.frame >= done_f and self.reveal_end_ms > 1.0e299 then self.reveal_end_ms = current_ms - (K_WAIT_EFFECT + K_WAIT_NEXT) - 1 end end function ResultPlayer:draw() NK_RESULT_DRAW_MS = tex.get_current_ms() if not (self.owner and self.movie) then return end self.movie:draw(texture, SIZE, K, 0, 0, 1, texture_w, ADDITIVE) self:draw_title() end local function outlined(str, size) if not str or str == "" then return false end return text.create_raw_text(str, math.floor(size * K + 0.5), { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4.5 * K / SS, 0.0) or false end local T8_TITLE = { right = 494, left = 165, y = 41, size = 25, fill = { 255, 248, 225, 255 }, ring = { 240, 168, 32, 255 }, edge = { 40, 28, 16, 255 }, ring_w = 3.5, edge_w = 5.5 } function ResultPlayer:draw_title() if GAME == "t8" then local c = T8_TITLE if self.title == nil then local str = tex.song_title() if str and str ~= "" then local px = math.floor(c.size * K + 0.5) self.title = text.create_raw_text(str, px, c.fill, c.ring, false, c.ring_w * K / SS, 0.0) or false self.title_back = text.create_raw_text(str, px, c.ring, c.edge, false, c.edge_w * K / SS, 0.0) or false else self.title = false end end local t, bk = self.title, self.title_back if t then local w = math.min(t.width * TITLE_SQUEEZE, (c.right - c.left) * K) local sq = w / t.width local cx, cy = c.right * K - w / 2, c.y * K if bk then local bw = bk.width * sq bk:draw({ x = floor(cx - bw / 2 + 0.5), y = floor(cy - bk.height / 2 + 0.5), x2 = bw - bk.width }) end t:draw({ x = floor(cx - w / 2 + 0.5), y = floor(cy - t.height / 2 + 0.5), x2 = w - t.width }) end return end if self.title == nil then self.title = outlined(tex.song_title(), TITLE.size) self.subtitle = outlined(tex.song_subtitle(), SUBTITLE.size) end local function centred(t, c) if not t then return end local w = math.min(t.width * TITLE_SQUEEZE, (640 - 24) * K) t:draw({ x = floor(c.x * K - w / 2 + 0.5), y = floor(c.y * K - t.height / 2 + 0.5), x2 = w - t.width }) end centred(self.subtitle, SUBTITLE) centred(self.title, TITLE) end function ResultPlayer:draw_gauge() end function ResultPlayer:chara_pos() return { -2000, -2000, 1.0 } end function ResultPlayer:nameplate_pos() return { -2000, -2000, 0.0 } end