Files
PyTaikoGreen/Scripts/ai_section_result/ai_section_result.lua
T
2026-11-06 00:21:39 -05:00

471 lines
18 KiB
Lua

AISectionBoard = {}
AISectionBoard.__index = AISectionBoard
local S = tex.get_screen_scale()
local FRAME_MS = 1000 / 60
local SECTION_END_WAIT, TOTAL_WAIT = 60, 60
local PLAYER_WAIT, DETAIL_WAIT = 600, 1200
local BADGE_FIRST_WAIT, BADGE_CHALLENGE_WAIT, BADGE_UNMET_WAIT = 60, 90, 60
local VOICE_WAIT, TOKEN_WAIT, WIN_WAIT = 60, 120, 210
local TOKEN_STEP, COUNT_FRAMES = 15, 30
local BOARD_FADE = 10
local RANK_MAX = 11
local LOGO_X, LOGO_DETAIL_X, LOGO_Y = 889, 1089, 502
local SIMPLE = {
[3] = { x = { 300, 428, 556 }, y = { 485, 539, 485 } },
[5] = { x = { 116, 244, 372, 500, 628 }, y = { 485, 539, 485, 539, 485 } },
}
local DETAIL = {
[3] = { x = { 322, 510.5, 699 }, y = { 510, 528, 510 } },
[5] = { x = { 168, 328, 488, 648, 808 }, y = { 510, 536, 510, 536, 510 } },
}
local ROW_COUNT_Y = { -1, 32, 65, 99 }
local ROW_DIFF_Y = { 0, 33, 66, 99 }
local ROW_KEYS = { "good", "ok", "bad", "drumroll" }
local function clamp01(v) return math.max(0, math.min(1, v)) end
local function ramp(t, at, len) return clamp01((t - at) / len) end
local function lerp(a, b, u) return a + (b - a) * u end
local function play(name)
if audio.has_sound(name) then audio.play_sound(name, name:find("^vo_") and "voice" or "sound") end
end
local function beats_best(now, best)
if now.good ~= best.good then return now.good > best.good end
if now.ok ~= best.ok then return now.ok > best.ok end
if now.bad ~= best.bad then return now.bad < best.bad end
return now.drumroll > best.drumroll
end
local function no_best(best)
return not best or (best.good == 0 and best.ok == 0 and best.bad == 0 and best.drumroll == 0)
end
function AISectionBoard.new(data)
local self = setmetatable({}, AISectionBoard)
self.sizes, self.ids = {}, {}
self.sections = data.sections
self.count = #data.sections
self.won = data.won
self.f = 0
self.started = false
tex.load_folder("ai_section_result", "ai_background")
tex.load_folder("ai_section_result", "ai_board")
if audio.has_sound("ai_bgm") then audio.play_sound("ai_bgm", "music") end
return self
end
function AISectionBoard:begin(progress)
self.progress = progress
self.first_play = progress == nil
if progress then
for i, section in ipairs(self.sections) do
section.best = progress.bests[i]
self.first_play = self.first_play or no_best(section.best)
end
end
self.best_updated = {}
for i, section in ipairs(self.sections) do
self.best_updated[i] = not self.first_play and beats_best(section, section.best)
end
self.start_ms = nil
self.f = 0
self.played = {}
self.total_at = self:simple_end()
self.rank_at = self.total_at + TOTAL_WAIT
self.rank_events = self:rank_schedule()
self.wait_at = self.rank_at + self.rank_events.finish
self.mode = "simple"
self.mode_at = 0
self.leaving = false
self.detail_seen_at = nil
self.deadline = self.wait_at + PLAYER_WAIT
self.started = true
play("vo_call")
end
function AISectionBoard:simple_end()
return 15 + SECTION_END_WAIT
end
function AISectionBoard:rank_schedule()
local p = self.progress
local events = { finish = 0 }
if not p or not self.won then return events end
local t = 0
if p.tokens_after ~= p.tokens_before and p.rank_before < RANK_MAX then
if p.challenge then
t = t + BADGE_CHALLENGE_WAIT
events.challenge = t
end
if p.first then
t = t + BADGE_FIRST_WAIT
events.first = t
end
t = t + VOICE_WAIT
if p.first or p.challenge then events.voice = t end
t = t + TOKEN_WAIT
events.tokens = t
t = t + (p.tokens_after - p.tokens_before) * TOKEN_STEP
events.tokens_end = t
elseif not p.fulfilled and p.rank_before < RANK_MAX then
t = t + BADGE_UNMET_WAIT
events.unmet = t
end
t = t + WIN_WAIT
events.wins = t
events.finish = t + COUNT_FRAMES
return events
end
function AISectionBoard:skip_reveal()
if self.f < self.wait_at then
self.f = self.wait_at
self.start_ms = self.now_ms - self.f * FRAME_MS
for _, key in ipairs({ "popup", "marks", "logo", "challenge", "first", "unmet", "voice" }) do
self.played[key] = true
end
return true
end
return false
end
function AISectionBoard:decide()
if self.leaving then return false end
if self:skip_reveal() then return true end
if self.mode == "detail" and self.f < self.mode_at + self:detail_length() then
self.mode_at = self.f - self:detail_length()
return true
end
self.leaving = true
return true
end
function AISectionBoard:toggle()
if self.leaving or self.f < self.wait_at then return false end
self.mode = self.mode == "simple" and "detail" or "simple"
self.mode_at = self.f
self.deadline = self.f + DETAIL_WAIT
return true
end
function AISectionBoard:detail_length()
return BOARD_FADE + 2 * COUNT_FRAMES + 20
end
function AISectionBoard:once(key, at)
if not self.played[key] and self.f >= at then
self.played[key] = true
return true
end
return false
end
function AISectionBoard:update(ms)
self.now_ms = ms
if not self.started then return end
self.start_ms = self.start_ms or ms
self.f = (ms - self.start_ms) / FRAME_MS
if self:once("popup", 5) then play("se_popup") end
if self:once("marks", 7) then play("se_section_mark") end
if self:once("logo", self.total_at) then play(self.won and "se_logo_win" or "se_logo_lose") end
local e, rank_t = self.rank_events, self.rank_at
if e.challenge and self:once("challenge", rank_t + e.challenge) then play("se_badge") end
if e.first and self:once("first", rank_t + e.first) then
play("se_badge")
if e.challenge then play("se_badge_add") end
end
if e.unmet and self:once("unmet", rank_t + e.unmet) then play("se_badge") end
if e.voice and self:once("voice", rank_t + e.voice) then play("vo_bonus") end
if e.tokens then
local steps = math.floor((self.f - rank_t - e.tokens) / TOKEN_STEP)
if steps >= 0 and steps < self.progress.tokens_after - self.progress.tokens_before
and self:once("token" .. steps, rank_t + e.tokens + steps * TOKEN_STEP) then
play("se_count")
end
end
if self:rankup() and e.tokens_end then
if self:once("rankup", rank_t + e.tokens_end) then play("se_rankup_panel") end
if self:once("rankup_shine", rank_t + e.tokens_end + 65) then play("se_rankup_shine") end
end
if e.wins and self:once("wins", rank_t + e.wins) then play("se_win_count") end
if self.mode == "detail" and not self.detail_seen_at then self.detail_seen_at = self.mode_at end
if self.mode == "detail" then
local t = self.f - self.mode_at
if t >= BOARD_FADE and t < BOARD_FADE + COUNT_FRAMES and self:once("detail_count", self.mode_at + BOARD_FADE) then
play("se_count")
end
if not self.first_play then
for _, updated in ipairs(self.best_updated) do
if updated and self:once("best", self.mode_at + BOARD_FADE + 2 * COUNT_FRAMES) then play("se_best") end
end
end
end
if self.f >= self.deadline then self.leaving = true end
end
function AISectionBoard:rankup()
local p = self.progress
return p and p.next_rank_tokens >= 0 and p.tokens_after >= p.next_rank_tokens
end
function AISectionBoard:is_done()
return self.leaving
end
function AISectionBoard:put(folder, key, cx, cy, o)
o = o or {}
local path = folder .. "/" .. key
local id = self.ids[path]
if id == nil then
id = tex.get_texture("ai_section_result/" .. path)
self.ids[path] = id
self.sizes[path] = tex.get_texture_info(folder, key)
end
local info = self.sizes[path]
if not info then return end
local sx, sy = o.sx or o.s or 1, o.sy or o.s or 1
local w, h = info.width * S, info.height * S
tex.draw_texture(id, {
x = cx * S, y = cy * S,
x2 = w * (sx - 1), y2 = h * (sy - 1),
origin = { w * sx * 0.5, h * sy * 0.5 },
rotation = o.rot or 0,
fade = o.fade or 1,
frame = o.frame or 0,
mirror = o.mirror,
blend = o.add and "additive" or nil,
})
end
function AISectionBoard:board(key, cx, cy, o)
self:put("ai_board", key, cx, cy, o)
end
function AISectionBoard:draw_background()
self.bg_start = self.bg_start or self.now_ms or 0
local f = ((self.now_ms or 0) - self.bg_start) / FRAME_MS
local bg = "ai_background"
if self.won then
self:put(bg, "gradient_win", 640, 360, { sx = 160 })
for k = 0, 1 do
local u = ((f * 1.35 + k * 425) % 850) / 850
self:put(bg, "pattern", 640, 844 - u * 850, {
fade = 0.13 * math.sin(math.pi * u), add = true, mirror = k == 0 and "horizontal" or nil,
})
end
for k = 0, 2 do
local u = ((f * 0.5 + k * 280) % 840) / 840
self:put(bg, "triangle", 300 + k * 340, 800 - u * 880, { rot = f * 1.7 + k * 120, fade = 0.4 })
end
else
self:put(bg, "gradient_lose", 640, 360, { sx = 160 })
for k = 0, 2 do
local u = ((f * 1.55 + k * 640) % 1920) / 1920
self:put(bg, "pattern", -320 + u * 1920, 364, { fade = 0.13 * math.sin(math.pi * u), add = true })
end
end
end
function AISectionBoard:draw_simple(fade)
local layout = SIMPLE[self.count]
if not layout or fade <= 0 then return end
local t = self.f
for i, section in ipairs(self.sections) do
local x, y = layout.x[i], layout.y[i]
self:board("section_back", x, y, { fade = fade })
self:board("section_back_top", x, y, { fade = fade })
if section.won then
local s = t < 7 and 1 or t < 10 and lerp(0.95, 1.05, ramp(t, 7, 3)) or t < 13 and lerp(1.05, 1, ramp(t, 10, 3)) or 1
self:board("section_win", x, y, { s = s, fade = fade * ramp(t, 5, 1) })
if t >= 7 and t < 17 then
self:board("section_flash", x, y, { s = lerp(1, 1.5, ramp(t, 7, 10)), fade = fade * (1 - ramp(t, 7, 10)) })
end
else
self:board("section_lose", x, y, { fade = fade * ramp(t, 5, 8) })
end
self:board("section_name", x - 2, y - 73, { frame = i - 1, fade = fade * ramp(t, 5, 1) })
end
end
-- Right aligned digits; slots step left from the anchor.
function AISectionBoard:draw_number(key, value, right_x, y, step, fade)
value = math.max(0, math.floor(value))
local x = right_x
repeat
self:board(key, x, y, { frame = value % 10, fade = fade })
value = value // 10
x = x - step
until value == 0
end
function AISectionBoard:draw_detail(fade)
local layout = DETAIL[self.count]
if not layout or fade <= 0 then return end
local t = self.f - self.mode_at - BOARD_FADE
local count_u = ramp(t, 0, COUNT_FRAMES)
local diff_u = ramp(t, COUNT_FRAMES, COUNT_FRAMES)
for i, section in ipairs(self.sections) do
local x, y = layout.x[i], layout.y[i]
self:board("detail_panel", x, y, { fade = fade })
self:board(section.won and "detail_win" or "detail_lose", x + 37, y - 76, { fade = fade })
self:board("section_name", x - 38, y - 76, { frame = i - 1, fade = fade })
for row, key in ipairs(ROW_KEYS) do
self:draw_number("digits_count", section[key] * count_u, x + 20, y - 2 + ROW_COUNT_Y[row], 17, fade)
if not self.first_play and diff_u > 0 then
local diff = section[key] - section.best[key]
local sign = diff > 0 and "sign_plus" or diff < 0 and "sign_minus" or "sign_even"
self:board(sign, x + 36, y - 2 + ROW_DIFF_Y[row], { fade = fade * diff_u })
self:draw_number("digits_diff", math.abs(diff) * diff_u, x + 65, y - 2 + ROW_DIFF_Y[row], 10, fade * diff_u)
end
end
if self.best_updated[i] then
local best_fade = ramp(t, 2 * COUNT_FRAMES, 10)
if best_fade > 0 then self:board("best_updated", x, y - 33, { fade = fade * best_fade }) end
end
end
end
function AISectionBoard:draw_logo(x, fade)
if fade <= 0 then return end
local t = self.f - self.total_at
local y = LOGO_Y
if self.won then
local pulse = t < 14 and 1.087 or lerp(1.087, 0.725, math.abs(((t - 14) % 118) / 59 - 1))
self:board("win_glow", x, y, { s = pulse, fade = fade * ramp(t, 2, 12) })
if t >= 14 then
self:board("win_ring_glow", x, y, { add = true, fade = fade * lerp(0.2, 1, math.abs(((t - 14) % 118) / 59 - 1)) })
end
if t >= 9 then
self:board("win_laurel", x, y, { s = lerp(1.268, 1, ramp(t, 9, 5)), fade = fade * ramp(t, 9, 5) })
self:board("win_rays", x, y, { s = 0.85, rot = t * 3, add = true, fade = fade * 0.3 * ramp(t, 9, 5) })
end
if t >= 9 and t < 14 then
self:board("win_flash", x, y, { s = lerp(1.087, 1.812, ramp(t, 9, 5)), fade = fade * (1 - ramp(t, 9, 5)) })
end
for k, key in ipairs({ "triangle_yellow", "triangle_pale", "triangle_purple", "triangle_pink", "triangle_white" }) do
local u = ramp(t, 23 + k * 2, 22)
if u > 0 and u < 1 then
local a = (k * 72) * math.pi / 180
self:board(key, x + math.cos(a) * 170 * u, y + math.sin(a) * 170 * u, { rot = t * 8, fade = fade * (1 - u) })
end
end
local logo_s = t < 6 and lerp(1, 2, ramp(t, 0, 6)) or lerp(1.772, 1, ramp(t, 6, 8))
self:board("logo_win", x, y, { s = logo_s, fade = fade * ramp(t, 0, 5) })
for k, spot in ipairs({ { 156, -107 }, { -167, 121 }, { -11, -159 }, { 140, 136 } }) do
local at = 29 + (k - 1) * 15
if t >= at then
local u = ((t - at) % 170) / 60
if u < 1 then self:board("sparkle", x + spot[1], y + spot[2], { fade = fade * math.sin(math.pi * u) }) end
end
end
else
if t < 13 then
self:board("lose_burst", x, y, { s = lerp(0.145, 1.8, ramp(t, 0, 12)), fade = fade * (1 - ramp(t, 6, 6)) })
end
local show = fade * ramp(t, 6, 7)
self:board("lose_disc", x, y, { fade = show })
self:board("lose_ring", x, y, { rot = -t * 0.75, fade = show })
self:board("lose_ring_thin", x, y, { rot = t * 0.75, fade = show })
self:board("logo_lose", x, y, { fade = show })
end
end
function AISectionBoard:draw_rank(fade)
local p = self.progress
if not p or fade <= 0 then return end
local e, t = self.rank_events, self.f - self.rank_at
local rankup = self:rankup() and e.tokens_end and t >= e.tokens_end
local rank = rankup and p.rank_after or p.rank_before
if rank >= RANK_MAX then
self:board("rank_panel_max", 1159, 440, { fade = fade })
else
self:board("rank_panel", 1159, 440, { fade = fade })
self:draw_number("digits_rank", rank, 1214 + (rank >= 10 and 17 or 0), 424, 35, fade)
if p.next_rank_tokens >= 0 then
local tokens = p.tokens_before
if e.tokens and t >= e.tokens then
tokens = math.min(p.tokens_after, p.tokens_before + math.floor((t - e.tokens) / TOKEN_STEP) + 1)
end
self:draw_number("digits_count", math.max(0, p.next_rank_tokens - tokens), 1201, 459, 17, fade)
end
end
local wins = p.wins_before
if e.wins and t >= e.wins then wins = lerp(p.wins_before, p.wins_after, ramp(t, e.wins, COUNT_FRAMES)) end
local to_next = p.next_reward_wins >= 0 and p.next_reward_wins - math.floor(wins) or nil
self:board(to_next and "wins_panel_next" or "wins_panel", 1160, 522, { fade = fade })
self:draw_number("digits_wins", wins, 1244, 508, 17, fade)
if to_next then self:draw_number("digits_count", to_next, 1218, 543, 17, fade) end
if e.wins and p.reward_kind ~= "" and t >= e.wins + COUNT_FRAMES then
local r = t - e.wins - COUNT_FRAMES
self:board("reward_text", 1160, 522, { s = r < 10 and lerp(1.5, 1, ramp(r, 0, 10)) or 1, fade = fade * ramp(r, 0, 10) })
end
local slide = function(at) return lerp(225, 0, ramp(t, at + 1, 6)), lerp(0.5, 1, ramp(t, at + 1, 6)) end
if e.challenge and t >= e.challenge then
local dx, a = slide(e.challenge)
self:board("badge_challenge", 1150 + dx, 379, { fade = fade * a })
end
if e.unmet and t >= e.unmet then
local dx, a = slide(e.unmet)
self:board("badge_unmet", 1150 + dx, 379, { fade = fade * a })
end
if e.first and t >= e.first then
local dx, a = slide(e.first)
self:board("badge_first", 1171 + dx, p.challenge and 345 or 383, { fade = fade * a })
end
if rankup then
local r = t - e.tokens_end - 65
if r >= 0 then
local s = r < 10 and lerp(1, 1.5, ramp(r, 0, 10)) or r < 14 and lerp(1.375, 1, ramp(r, 10, 4)) or 1
local a = ramp(r, 0, 10) * fade
self:board("rankup_glow", 1160, 440, { s = s + (r >= 14 and 0.02 * math.sin(r / 11) or 0), fade = a })
self:board("rankup_text", 1160, 440, { s = s, fade = a })
end
end
end
function AISectionBoard:draw_hints()
local t = self.f % 120
local simple = self.mode == "simple"
self:board("hint_end", 1169, 687)
self:board(simple and "hint_detail" or "hint_back", 930, 687)
self:board("drum", 810, 688)
self:board("drum", 1053, 688)
local rim = t < 60 and 1 - math.abs(t / 30 - 1) or 0
self:board("drum_rim", 798, 688, { fade = rim })
self:board("drum_rim", 822, 688, { fade = rim, mirror = "horizontal" })
local arrow = t < 60 and 1 - t / 60 or 0
self:board("drum_arrow", 783 - 7 * (1 - arrow), 688, { fade = arrow })
self:board("drum_arrow", 837 + 7 * (1 - arrow), 688, { fade = arrow, mirror = "horizontal" })
self:board("drum_center", 1053, 688, { fade = rim })
end
function AISectionBoard:draw()
if not self.started then return end
local since = self.f - self.mode_at
local detail = self.mode == "detail"
local logo_u = ramp(self.f - self.total_at, 1, 8)
if detail then
local out = 1 - ramp(since, 0, 4)
self:draw_simple(out)
self:draw_rank(out)
self:draw_logo(LOGO_X, logo_u * out)
self:draw_logo(LOGO_DETAIL_X, ramp(since, 2, 7))
self:draw_detail(ramp(since, 1, BOARD_FADE))
else
local back = self.detail_seen_at and ramp(since, 0, 4) or 1
self:draw_simple(back)
self:draw_rank(back)
self:draw_logo(LOGO_X, math.min(logo_u, back))
end
if self.f >= self.wait_at then self:draw_hints() end
end