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

434 lines
17 KiB
Lua

AIRewardFlow = {}
AIRewardFlow.__index = AIRewardFlow
local S = tex.get_screen_scale()
local FRAME_MS = 1000 / 60
local DIR = "ai_reward"
local KATSU, DON = 1, 2
local RANK_MAX, ASSIGNED_RANK = 11, 10
local ALL_REWARDS_WINS = 9999
local INIT_WAIT = 20
local RANK_COUNT_WAIT, TITLE_WAIT, PANEL_FADE, PLAYER_WAIT = 60, 150, 20, 1200
local REWARD_WAIT, NEXT_WAIT, HINT_WAIT, LONG_WAIT = 120, 120, 120, 1800
local CONDITION_BACK = { [0] = 0, 0, 0, 1, 1, 2, 2, 3, 3, 3 }
local KIND_TEXT = { song = "ai_reward_song", head = "ai_reward_head", body = "ai_reward_body", title = "ai_reward_title" }
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 play_bgm(name)
if audio.has_sound(name) then audio.play_sound(name, "music") end
end
function AIRewardFlow.new(p)
local self = setmetatable({}, AIRewardFlow)
self.ids, self.sizes = {}, {}
self.p = p
self.stages = {}
if p.rank_after > p.rank_before then table.insert(self.stages, "rankup") end
if p.reward_kind ~= "" then table.insert(self.stages, "reward") end
local has_goal = p.rank_after < RANK_MAX or p.next_reward_wins >= 0
if #self.stages == 0 and has_goal then table.insert(self.stages, "remind") end
self.stage_index = 0
if #self.stages > 0 then
tex.load_folder("ai_section_result", DIR)
audio.stop_sound("ai_bgm")
end
if p.reward_name ~= "" then
self.reward_name = text.create_raw_text(p.reward_name, 38, { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 5, 2)
self.reward_kind = text.create_text(KIND_TEXT[p.reward_kind], { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 5, 2)
end
if p.next_reward_name ~= "" then
self.next_name = text.create_raw_text(p.next_reward_name, 24, { 0, 0, 0, 255 }, { 255, 255, 255, 255 }, false, 3, 2)
end
self:next_stage(0)
return self
end
function AIRewardFlow:next_stage(ms)
self.stage_index = self.stage_index + 1
self.stage = self.stages[self.stage_index]
self.start_ms = ms
self.f = 0
self.played = {}
self.phase = "reveal"
self.phase_at = 0
self.waiting_since = nil
if self.bgm then audio.stop_sound(self.bgm) end
self.bgm = self.stage and self.stage .. "_bgm"
if self.bgm then play_bgm(self.bgm) end
if self.stage == "reward" and self.p.reward_kind ~= "" then
play(math.random() < 0.5 and "vo_reward_congra1" or "vo_reward_congra2")
end
end
function AIRewardFlow:once(key, at)
if not self.played[key] and self.f >= at then
self.played[key] = true
return true
end
return false
end
function AIRewardFlow:set_phase(phase)
self.phase = phase
self.phase_at = self.f
end
function AIRewardFlow:update(ms)
if not self.stage then return end
self.now_ms = ms
if self.start_ms == 0 then self.start_ms = ms end
self.f = (ms - self.start_ms) / FRAME_MS
if self.stage == "rankup" then self:update_rankup() elseif self.stage == "reward" then self:update_reward() end
self:update_wait(ms)
end
function AIRewardFlow:update_rankup()
local p, shine_at = self.p, INIT_WAIT + RANK_COUNT_WAIT
if self:once("shine", shine_at) then
play("se_rankup_shine")
play("vo_rankup_odd")
if p.rank_after >= RANK_MAX then play("fanfare") end
end
if self.phase == "reveal" and self.f >= shine_at + TITLE_WAIT then
if p.rank_after == ASSIGNED_RANK then play("vo_rankup_congra1") end
if p.rank_after == RANK_MAX then play("vo_rankup_congra2") end
self:set_phase("title")
end
end
function AIRewardFlow:update_reward()
if self:once("get", INIT_WAIT + REWARD_WAIT) then
play(self.p.wins_after >= ALL_REWARDS_WINS and "vo_reward_complete" or "vo_reward_get")
end
if self.phase == "reveal" and self.f >= INIT_WAIT + REWARD_WAIT + NEXT_WAIT + HINT_WAIT then self:set_phase("wait") end
end
function AIRewardFlow:update_wait(ms)
if self.stage == "remind" and self.phase == "reveal" and self.f >= INIT_WAIT + HINT_WAIT then self:set_phase("wait") end
local limit = self.stage == "rankup" and PLAYER_WAIT or LONG_WAIT
if self.phase ~= "reveal" and self.f >= self.phase_at + limit then self:advance(ms) end
end
function AIRewardFlow:advance(ms)
if self.stage == "rankup" and self.phase == "title" then
if self.p.rank_after < RANK_MAX then play("vo_rankup_next") end
self:set_phase("next")
return
end
self:next_stage(ms)
end
function AIRewardFlow:input(action)
if not self.stage or self.f < INIT_WAIT then return false end
if self.phase == "reveal" then
if action ~= DON then return false end
if self.stage == "rankup" then
self.f = INIT_WAIT + RANK_COUNT_WAIT + TITLE_WAIT
self.played.shine = true
elseif self.stage == "reward" then
self.f = INIT_WAIT + REWARD_WAIT + NEXT_WAIT + HINT_WAIT
self.played.get = true
else
self.f = INIT_WAIT + HINT_WAIT
end
self.start_ms = self.now_ms and self.now_ms - self.f * FRAME_MS or self.start_ms
return true
end
local wanted = self.stage == "rankup" and self.phase == "title" and KATSU or DON
if action ~= wanted then return false end
self:advance(self.now_ms or self.start_ms)
return true
end
function AIRewardFlow:is_done()
return self.stage == nil
end
function AIRewardFlow:put(key, cx, cy, o)
o = o or {}
local id = self.ids[key]
if id == nil then
id = tex.get_texture("ai_section_result/" .. DIR .. "/" .. key)
self.ids[key] = id
self.sizes[key] = tex.get_texture_info(DIR, key)
end
local info = self.sizes[key]
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 AIRewardFlow:at(key, x, y, o)
local info = self.sizes[key]
if info == nil then
self:put(key, -9999, -9999, { fade = 0 })
info = self.sizes[key]
end
if not info then return end
self:put(key, x + info.width * 0.5, y + info.height * 0.5, o)
end
function AIRewardFlow:digits(key, value, cx, cy, step, o, glow)
local str = tostring(math.max(0, math.floor(value)))
local x = cx - (#str - 1) * step * 0.5
for i = 1, #str do
local d = str:byte(i) - 48
if glow then
local a = o and o.fade or 1
self:put(glow, x, cy, { frame = d, fade = a })
end
self:put(key, x, cy, { frame = d, fade = o and o.fade or 1, s = o and o.s })
x = x + step
end
end
function AIRewardFlow:draw_window(t)
local y_s = t < 2 and lerp(0.013, 0.194, ramp(t, 0, 2)) or t < 9 and lerp(0.375, 1.461, ramp(t, 2, 7)) or t < 11 and lerp(1.231, 1, ramp(t, 9, 2)) or 1
local x_s = t < 2 and lerp(0.073, 0.189, ramp(t, 0, 2)) or t < 9 and lerp(0.305, 1, ramp(t, 2, 7)) or 1
local a = t < 9 and ramp(t, 0, 8) or 1
local swing = (t % 240) / 120
local border = swing < 1 and swing or 2 - swing
self:put("window_border_a", 640, 330, { sx = x_s, sy = y_s, fade = a * (1 - border) })
self:put("window_border_b", 640, 330, { sx = x_s, sy = y_s, fade = a * border })
self:put("window_body", 640, 330, { sx = x_s, sy = y_s, fade = a })
end
function AIRewardFlow:draw_makina(t, x, y)
local f = t % 500
local frame, dx = 0, 0
if (f >= 66 and f < 73) or (f >= 127 and f < 134) then frame = 1 end
if f >= 184 then frame, dx = 2, 3 end
if f >= 274 then frame = 3 end
if f >= 499 then frame = 4 end
self:at("makina", x + dx, y, { frame = frame })
end
function AIRewardFlow:draw_ai_don(t, x, y)
local f = t % 250
local dy = 0
if f >= 88 and f < 115 then
dy = f < 101 and lerp(-10, -132, ramp(f, 88, 12)) or lerp(-122, 0, ramp(f, 101, 13))
end
self:at("ai_don", x, y + dy)
end
function AIRewardFlow:draw_burst(cx, cy, t)
if t < 0 then return end
local pulse = 0.725 + 0.362 * math.abs(((t + 60) % 120) / 60 - 1)
self:put("burst_glow", cx, cy, { s = pulse, fade = ramp(t, 0, 12) * (t > 13 and 0.5 + 0.5 * math.abs(((t - 13) % 120) / 60 - 1) or 1) })
self:put("burst_circle", cx, cy, { s = lerp(2, 1, math.abs(((t - 12) % 120) / 60 - 1)), fade = t >= 12 and 1 or 0 })
if t >= 7 then
self:put("burst_rays", cx, cy, { s = 0.85, rot = t * 3, add = true, fade = 0.3 * ramp(t, 7, 6) })
self:put("burst_rays_thin", cx, cy, { s = 0.85, rot = -t * 2, fade = 0.5 * ramp(t, 7, 6) })
end
for k, key in ipairs({ "triangle_yellow_small", "triangle_yellow", "triangle_purple", "triangle_pink", "triangle_white" }) do
local u = ((t - 11 + k * 3) % 121) / 34
if u < 1 then
local a = k * 72 * math.pi / 180
self:put(key, cx + math.cos(a) * 170 * u, cy + math.sin(a) * 170 * u, { rot = t * 8, fade = 1 - u })
end
end
local s = (t % 120) / 60
if s < 1 then self:put("sparkle", cx + 100, cy - 90, { fade = math.sin(math.pi * s) }) end
end
function AIRewardFlow:draw_don_hint(x, y, t)
local a = ramp(t, 0, 15)
local c = t % 120
local pulse = a * (c < 60 and 1 - math.abs(c / 30 - 1) or 0)
self:at("don_hint_glow", x + 47, y, { fade = a * 0.4 })
self:at("hint_glow", x, y - 10, { fade = pulse })
self:at("hint_end", x + 87, y + 12, { fade = a })
self:at("drum", x + 16, y + 6, { fade = a })
self:at("drum_center", x + 16, y + 6, { fade = pulse })
end
function AIRewardFlow:draw_katsu_hint(t)
local a = ramp(t, 0, 15)
local c = t % 120
local rim = a * (c < 60 and 1 - math.abs(c / 30 - 1) or 0)
local arrow = c < 60 and a * (1 - c / 60) or 0
self:at("hint_page", 865, 611, { fade = a })
self:put("katsu_drum", 733, 620, { fade = a })
self:put("katsu_rim", 721, 620, { fade = rim })
self:put("katsu_rim", 745, 620, { fade = rim, mirror = "horizontal" })
self:put("katsu_arrow", 760 + 7 * (c < 60 and c / 60 or 1), 620, { fade = arrow, mirror = "horizontal" })
self:put("katsu_arrow", 706 - 7 * (c < 60 and c / 60 or 1), 620, { fade = arrow })
end
function AIRewardFlow:draw_rank_number(rank, t, shine)
local cx, cy = 742, 218
if rank >= RANK_MAX then
self:put("rank_ten_plus_glow", 787, 211, { fade = 0.5 + 0.5 * math.abs((t % 40) / 20 - 1) })
self:put("rank_ten_plus", 787, 211)
return
end
local str = tostring(rank)
local x = cx - (#str - 1) * 50
for i = 1, #str do
local d = str:byte(i) - 48
if shine and t < 20 then self:put("rank_digit_glow", x, cy, { frame = d, fade = 1 - t / 20 }) end
self:put("rank_digit", x, cy, { frame = d })
x = x + 100
end
end
function AIRewardFlow:draw_next_rank(rank, a)
if rank >= RANK_MAX then
self:put("strongest_glow", 640, 438, { fade = a * (0.5 + 0.5 * math.abs((self.f % 60) / 30 - 1)) })
self:put("strongest_panel", 640, 438, { fade = a })
self:put("strongest_text", 640, 438, { fade = a })
return
end
self:at("next_panel", 300, 291.2, { fade = a })
if rank >= ASSIGNED_RANK then
self:at("next_max_text", 364, 320.2, { fade = a })
self:at("point_text", 795, 320.2, { fade = a })
self:digits("digits_next", self.p.next_rank_after_tokens - self.p.tokens_after, 724, 352, 42, { fade = a })
self:at("assigned_condition", 351.3, 378.3, { fade = a })
else
self:at("next_rank_text", 364, 320.2, { fade = a })
self:digits("digits_next", self.p.next_rank_after_tokens - self.p.tokens_after, 701, 352, 50, { fade = a })
self:at("condition_back", 351.3, 378.3, { frame = CONDITION_BACK[rank], fade = a })
self:at("condition_strip", 353.3, 395.3, { frame = rank, fade = a })
end
self:at("condition_note", 312.3, 494.3, { fade = a })
end
function AIRewardFlow:draw_rankup()
local p, t = self.p, self.f
self:draw_window(t)
local shine_at = INIT_WAIT + RANK_COUNT_WAIT
local after = t >= shine_at
self:draw_burst(742, 211, t - shine_at)
self:at("rank_label", 379, 160)
self:draw_rank_number(after and p.rank_after or p.rank_before, t - shine_at, after)
if self.phase == "title" or self.phase == "next" then
local since = t - self.phase_at
local a = self.phase == "title" and ramp(since, 0, PANEL_FADE) or 1 - ramp(since, 0, PANEL_FADE)
if a > 0 then
self:at("title_box", 316, 320, { fade = a })
self:at("rank_title", 316, 396, { frame = p.rank_after - 1, fade = a })
end
if self.phase == "title" and since >= PANEL_FADE then self:draw_katsu_hint(since - PANEL_FADE) end
end
if self.phase == "next" then
local since = t - self.phase_at
self:draw_next_rank(p.rank_after, ramp(since, PANEL_FADE, PANEL_FADE))
if since >= 2 * PANEL_FADE then self:draw_don_hint(734, 582, since - 2 * PANEL_FADE) end
end
self:draw_makina(t, 952.3, 249.9)
self:draw_ai_don(t, 33.4, 464)
end
function AIRewardFlow:draw_reward()
local p, t = self.p, self.f
self:draw_window(t)
self:draw_burst(617, 204, t - 12)
if t >= 16 then
self:at("total_text", 339, 181)
self:at("win_text", 750, 171)
self:at("achieved_text", 827, 181)
if p.wins_after >= 1000 then
self:digits("wins_four", p.wins_after, 612, 215, 70)
else
self:digits("wins_three", p.wins_after, 612, 215, 97)
end
end
local panel_at = INIT_WAIT + REWARD_WAIT
if t >= panel_at then
local a = ramp(t, panel_at, PANEL_FADE)
self:at("reward_box", 288, 266, { fade = a })
if self.reward_name then
local fade = ramp(t, panel_at, 10)
self.reward_kind:draw({ x = (640 - self.reward_kind.width / (2 * S)) * S, y = 340 * S, fade = fade })
self.reward_name:draw({ x = (640 - self.reward_name.width / (2 * S)) * S, y = 375 * S, fade = fade })
end
end
local next_at = panel_at + NEXT_WAIT
if t >= next_at then
local a = ramp(t, next_at, PANEL_FADE)
if p.all_rewards or p.next_reward_wins < 0 then
self:at("all_rewards_bar", 299, 441.25, { fade = a })
else
self:at("next_reward_bar", 301, 450.2, { fade = a })
if self.next_name then
self.next_name:draw({ x = (610 - self.next_name.width / (2 * S)) * S, y = (490 - self.next_name.height / (2 * S)) * S, fade = a })
end
self:at("win_text_small", 933, 468.2, { fade = a })
self:digits("digits_need", p.next_reward_wins - p.wins_after, 905, 490, 32, { fade = a }, "digits_need_glow")
end
end
if self.phase == "wait" then self:draw_don_hint(745, 587, t - self.phase_at) end
self:draw_makina(t, 952.3, 249.9)
self:draw_ai_don(t, 45, 465.5)
end
function AIRewardFlow:draw_goal_rank(x, y, a)
local p = self.p
self:at("goal_panel", x, y, { fade = a })
self:at("goal_rank_text", x, y + 62, { fade = a })
self:at("rank_title_small", x + 212, y + 152, { frame = p.rank_after, fade = a })
self:digits("digits_goal", p.next_rank_after_tokens - p.tokens_after, x + 432, y + 103, 50, { fade = a })
end
function AIRewardFlow:draw_goal_wins(x, y, a)
local p = self.p
self:at("goal_panel", x, y, { fade = a })
self:at("goal_wins_text", x, y + 62, { fade = a })
if self.next_name then
self.next_name:draw({ x = (x + 376 - self.next_name.width / (2 * S)) * S, y = (y + 170 - self.next_name.height / (2 * S)) * S, fade = a })
end
self:digits("digits_goal", p.next_reward_wins - p.wins_after, x + 539, y + 103, 50, { fade = a })
end
function AIRewardFlow:draw_remind()
local p, t = self.p, self.f
self:at("next_goal_text", 8, 18)
local a = ramp(t, INIT_WAIT, PANEL_FADE)
local has_rank = p.rank_after < RANK_MAX
local has_wins = p.next_reward_wins >= 0
if has_rank and has_wins then
self:draw_goal_rank(198, 104, a)
self:draw_goal_wins(292, 342, a)
elseif has_rank then
self:draw_goal_rank(264, 224, a)
else
self:draw_goal_wins(264, 224, a)
end
if self.phase == "wait" then
local h = t - self.phase_at
local fade = ramp(h, 0, 15)
self:at("remind_hint_end", 1093.1, 31.1, { fade = fade })
self:at("remind_drum", 1036, 26, { fade = fade })
local c = h % 120
self:at("remind_drum_center", 1036, 26, { fade = fade * (c < 60 and 1 - math.abs(c / 30 - 1) or 0) })
end
self:draw_makina(t, 952.3, 249.9)
self:draw_ai_don(t, 45, 473.1)
end
function AIRewardFlow:draw()
if self.stage == "rankup" then self:draw_rankup()
elseif self.stage == "reward" then self:draw_reward()
elseif self.stage == "remind" then self:draw_remind() end
end