Files
YataiDON14/Scripts/result/result_player.lua
T
2026-10-15 09:16:53 -04:00

285 lines
12 KiB
Lua

ResultPlayer = {}
ResultPlayer.__index = ResultPlayer
local VM = require("result.swg_vm")
local GAME = "t14"
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
-- This skin shows no song number / title on the result screen.
function ResultPlayer:draw_song_info() end
function ResultPlayer:chara_pos() return { -2000, -2000, 1.0 } end
function ResultPlayer:nameplate_pos() return { -2000, -2000, 0.0 } end