285 lines
12 KiB
Lua
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
|