- ai_plate: the AI lane's "AIどん" plate, drawn with the AI scoreboard (needs the engine's Player::use_ai_lane, which drops the lane's own plate / auto marker, course mark, and switches to the AI lane cover). - Section timer: frame, bar and labels moved to sit on the score plate (frame at 160,154; bar 189,166 150x16, drawn over the frame), bar colours from the timer art. - ai_game_song_info: the AI game's title / song number / genre plate at the bottom right. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
927 lines
39 KiB
Lua
927 lines
39 KiB
Lua
require("game/gauge")
|
|
|
|
AIBattleOverlay = {}
|
|
AIBattleOverlay.__index = AIBattleOverlay
|
|
|
|
local DIR = "ai_battle/"
|
|
local TILES = 19
|
|
local TILE_WIDTH = 72
|
|
local SECTIONS = 5
|
|
|
|
local GAUGE_SCALE = 0.5
|
|
local GAUGE_X, GAUGE_Y = 10, 15
|
|
|
|
local PROGRESS = { x = 189, y = 166, w = 150, h = 16 }
|
|
|
|
local S = tex.get_screen_scale()
|
|
local FRAME_MS = 1000 / 60
|
|
|
|
-- A triangle wave between 0 and 1.
|
|
local function flash(ms, period)
|
|
return 1 - math.abs(2 * ((ms % period) / period) - 1)
|
|
end
|
|
|
|
-- Linear value of a {frame, value} key list; outside the keys it holds the end value.
|
|
local function track(f, keys)
|
|
if f <= keys[1][1] then return keys[1][2] end
|
|
for i = 2, #keys do
|
|
local k0, k1 = keys[i - 1], keys[i]
|
|
if f <= k1[1] then
|
|
return k0[2] + (k1[2] - k0[2]) * (f - k0[1]) / (k1[1] - k0[1])
|
|
end
|
|
end
|
|
return keys[#keys][2]
|
|
end
|
|
|
|
-- Fades in and out over `length` frames: the stage flash the Lumen sprites share.
|
|
local function blink(f, length)
|
|
if f < 0 or f > length then return 0 end
|
|
return 1 - math.abs(2 * f / length - 1)
|
|
end
|
|
|
|
function AIBattleOverlay.new()
|
|
local self = setmetatable({}, AIBattleOverlay)
|
|
self.ids = {}
|
|
self.sizes = {}
|
|
self:reset()
|
|
return self
|
|
end
|
|
|
|
function AIBattleOverlay:id(key)
|
|
self.ids[key] = self.ids[key] or tex.get_texture(key)
|
|
return self.ids[key]
|
|
end
|
|
|
|
function AIBattleOverlay:reset()
|
|
self.wins = {}
|
|
self.section = 0
|
|
self.section_count = SECTIONS
|
|
self.event = nil -- latest section result: { won, start }
|
|
self.final = nil -- match result: { won, full_combo, start }
|
|
self.start_ms = nil
|
|
self.phase = 1
|
|
self.phase_start = nil
|
|
self.played = {}
|
|
self.flame_tier = {}
|
|
end
|
|
|
|
function AIBattleOverlay:set_section_result(section, won)
|
|
self.wins[section] = won
|
|
self.event = { won = won, section = section }
|
|
end
|
|
|
|
function AIBattleOverlay:set_match_result(won, full_combo)
|
|
self.final = { won = won, full_combo = full_combo }
|
|
end
|
|
|
|
function AIBattleOverlay:spr(key, cx, cy, o)
|
|
o = o or {}
|
|
local info = self.sizes[key]
|
|
if not info then
|
|
info = tex.get_texture_info("ai_battle", key)
|
|
self.sizes[key] = info
|
|
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(self:id(DIR .. key), {
|
|
x = cx * S, y = cy * S,
|
|
x2 = w * (sx - 1), y2 = h * (sy - 1),
|
|
origin = { w * sx * (o.ax or 0.5), h * sy * (o.ay or 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
|
|
|
|
local function stamp(event, ms)
|
|
if event and not event.start then event.start = ms end
|
|
end
|
|
|
|
function AIBattleOverlay:cue(name, preset, frame, at, tag)
|
|
local key = name .. (tag or "")
|
|
if frame >= (at or 0) and not self.played[key] then
|
|
self.played[key] = true
|
|
name = "ai_battle_" .. name
|
|
if audio.has_sound(name) then audio.play_sound(name, preset) end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:event_frame(ms)
|
|
if not self.event or not self.event.start then return nil end
|
|
local f = (ms - self.event.start) / FRAME_MS
|
|
return f < 179 and f or nil
|
|
end
|
|
|
|
local function pattern_phase(section, count)
|
|
if section >= count - 1 then return 3 end
|
|
return section >= count // 2 and 2 or 1
|
|
end
|
|
|
|
local COLUMNS = {
|
|
{ "pattern_column_triangle", 358, 0, 289, 814, 440 }, { "pattern_column_triangle", 1136, 0, 244, 439, 813 },
|
|
{ "pattern_column_wave", 852, 0, 299, 441, 812 }, { "pattern_column_wave", 1206, 0, 165, 811, 438 },
|
|
{ "pattern_column_wave", 73, 0, 236, 440, 809 }, { "pattern_column_wave", 499, 82, 136, 441, 812 },
|
|
{ "pattern_column_maze", 923, 58, 185, 811, 440 }, { "pattern_column_triangle", 640, 0, 299, 438, 811 },
|
|
{ "pattern_column_diamond", 568, 24, 259, 814, 438 }, { "pattern_column_diamond", 287, 24, 170, 437, 813 },
|
|
{ "pattern_column_diamond", 993, 55, 211, 437, 811 },
|
|
}
|
|
|
|
local QUADS = {
|
|
{ 0, 71, 133, { 40, 133, 216 }, 0.4 }, { 1, 589, 73, { 40, 158 }, 0.8 }, { 2, 835, 50, { 82, 201 }, 0.3 },
|
|
{ 3, 1047, 104, { 40, 223 }, 0.3 }, { 4, 454, 134, { 48, 191 }, 0.2 }, { 5, 708, 169, { 130, 259 }, 0.4 },
|
|
{ 6, 949, 134, { 245 }, 0.3 }, { 7, 444, 48, { 91 }, 0.2 }, { 8, 1257, 169, { 73, 258 }, 0.3 },
|
|
{ 9, 754, 101, { 139 }, 0.2 }, { 10, 321, 75, { 153, 248 }, 0.4 },
|
|
}
|
|
local LINES = { "line_upper_1", "line_upper_2", "line_upper_3", "line_upper_4", "line_upper_5" }
|
|
local LINE_Y = { 161, 133, 103, 75, 47 }
|
|
|
|
function AIBattleOverlay:draw_patterns(ms)
|
|
local phase = self.phase
|
|
local since = (ms - (self.phase_start or ms)) / FRAME_MS
|
|
local fade_in = math.min(since / 20, 1)
|
|
local scroll = (ms % 8500) / 8500 * 1152
|
|
local first = phase == 1 and 0.5 or (phase == 2 and 0.5 * (1 - math.min(since / 50, 1)) or 0)
|
|
local second = phase == 2 and 0.5 * fade_in or (phase == 3 and 0.5 * (1 - math.min(since / 50, 1)) or 0)
|
|
if first > 0 then
|
|
tex.draw_texture(self:id(DIR .. "lower_triangles_1"), { y = scroll, fade = first })
|
|
end
|
|
if second > 0 then
|
|
tex.draw_texture(self:id(DIR .. "lower_triangles_2"), { y = scroll, fade = second })
|
|
end
|
|
if phase < 2 then return end
|
|
|
|
local f = (ms / FRAME_MS) % 480
|
|
local rise = {
|
|
{ "pattern_swirl", 545, f - 73, 196 }, { "pattern_triangle", 239, f - 10, 200 },
|
|
{ "pattern_flower", 818, f, 135 }, { "pattern_triangle", 839, f - 267, 187 },
|
|
{ "pattern_triangle", 257, f - 311, 168 },
|
|
}
|
|
for _, r in ipairs(rise) do
|
|
if r[3] >= 0 and r[3] <= r[4] then
|
|
local p = r[3] / r[4]
|
|
self:spr(r[1], r[2], 794 - 324 * p, {
|
|
rot = (r[2] < 640 and 1 or -1) * 70 * p, fade = math.min(1 - p, fade_in), add = true,
|
|
})
|
|
end
|
|
end
|
|
if phase < 3 then return end
|
|
|
|
local g = (ms / FRAME_MS) % 300
|
|
self:spr("pattern_sprinkle", 642, 384 + 475 * g / 300, { fade = fade_in, add = true })
|
|
self:spr("pattern_sprinkle", 642, 384 + 475 * ((g + 150) % 300) / 300, { fade = fade_in, add = true })
|
|
for _, c in ipairs(COLUMNS) do
|
|
local p = g - c[3]
|
|
if p >= 0 and p <= c[4] then
|
|
self:spr(c[1], c[2], c[5] + (c[6] - c[5]) * p / c[4], { fade = fade_in, add = true })
|
|
end
|
|
end
|
|
for _, q in ipairs(QUADS) do
|
|
for _, start in ipairs(q[4]) do
|
|
local a = blink(g - start, 40)
|
|
if a > 0 then self:spr("flash_quad", q[2], q[3], { frame = q[1], fade = a * q[5], add = true }) end
|
|
end
|
|
end
|
|
for i, key in ipairs(LINES) do
|
|
local a = math.max(blink(g, 40), 0.7 * blink(g - 40 - 4 * i, 20))
|
|
if a > 0 then self:spr(key, 640, LINE_Y[i], { fade = a, add = true }) end
|
|
end
|
|
for i, y in ipairs({ 698, 650, 603, 553 }) do
|
|
local a = math.max(blink(g, 40), 0.7 * blink(g - 36 - 4 * i, 20), 0.7 * blink(g - 125 - 4 * i, 20))
|
|
if a > 0 then self:spr("line_lower", 640, y, { fade = a, add = true }) end
|
|
end
|
|
if since < 110 then
|
|
for i, x in ipairs({ 429, 641, 1066, 216, 499, 851, 1207, 74 }) do
|
|
local h = since - (i - 1) * 8
|
|
local a = blink(h, 60)
|
|
if a > 0 then
|
|
self:spr(i % 2 == 0 and "flash_tile_narrow" or "flash_tile_wide", x, 628, { fade = a, add = true })
|
|
self:spr(i % 2 == 0 and "flash_triangle_white" or "flash_triangle_yellow", x, 628 + 30 * (h / 60), {
|
|
s = 0.8 * (1 - 0.8 * h / 60), rot = 45 * h / 60, fade = a, add = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_back(ms, territory)
|
|
stamp(self.event, ms)
|
|
stamp(self.final, ms)
|
|
self.start_ms = self.start_ms or ms
|
|
|
|
local since_start = (ms - self.start_ms) / FRAME_MS
|
|
self:cue("se_enter", "sound", since_start)
|
|
self:cue("vo_begin", "voice", since_start, 45)
|
|
local event = self.event
|
|
if event and event.start then
|
|
local f = (ms - event.start) / FRAME_MS
|
|
if not self.final then
|
|
self:cue(event.won and "vo_section_win" or "vo_section_lose", "voice", f, 0, event.section)
|
|
end
|
|
self:cue(event.won and "se_section_win" or "se_section_lose", "sound", f, 10, event.section)
|
|
end
|
|
|
|
local phase = pattern_phase(self.section, self.section_count)
|
|
if phase ~= self.phase then
|
|
self.phase, self.phase_start = phase, ms
|
|
end
|
|
|
|
local red = math.min(math.max(1, territory + 10), TILES - 1)
|
|
local showing_highlight = true
|
|
local upper = "bg_upper"
|
|
local back_fade
|
|
local since_end
|
|
if self.final and self.final.start then
|
|
local f = (ms - self.final.start) / FRAME_MS
|
|
since_end = f
|
|
showing_highlight = false
|
|
red = self.final.won and math.min(f // 1 + 1, TILES) or math.max(TILES - f // 1 - 1, 0)
|
|
upper = self.final.won and "bg_final_upper" or "bg_lose_upper"
|
|
back_fade = 1 - math.min(f / 5, 1)
|
|
else
|
|
local f = self:event_frame(ms)
|
|
if f then
|
|
showing_highlight = false
|
|
red = self.event.won and math.min(f // 2 + 1, TILES) or math.max(TILES - f // 2 - 1, 0)
|
|
upper = self.event.won and "bg_win_upper" or "bg_lose_upper"
|
|
back_fade = 1 - math.min(f / 5, 1)
|
|
end
|
|
end
|
|
red = math.floor(red)
|
|
local blue = TILES - red
|
|
local highlight = flash(ms, 332)
|
|
|
|
tex.draw_texture(self:id(DIR .. "bg_lower"))
|
|
for i = 0, red - 1 do
|
|
tex.draw_texture(self:id(DIR .. "red_tile_lower"), { frame = i, x = i * TILE_WIDTH })
|
|
end
|
|
for i = 0, blue - 1 do
|
|
tex.draw_texture(self:id(DIR .. "blue_tile_lower"), { frame = i, x = (TILES - 1 - i) * TILE_WIDTH })
|
|
end
|
|
self:draw_patterns(ms)
|
|
if showing_highlight then
|
|
tex.draw_texture(self:id(DIR .. "highlight_tile_lower"), { x = red * TILE_WIDTH, fade = highlight })
|
|
end
|
|
|
|
tex.draw_texture(self:id(DIR .. upper))
|
|
if back_fade and back_fade > 0 then
|
|
tex.draw_texture(self:id(DIR .. "bg_upper"), { fade = back_fade })
|
|
end
|
|
for i = 0, red - 1 do
|
|
tex.draw_texture(self:id(DIR .. "red_tile_upper"), { frame = i, index = i })
|
|
end
|
|
for i = 0, blue - 1 do
|
|
tex.draw_texture(self:id(DIR .. "blue_tile_upper"), { frame = i, index = TILES - 1 - i })
|
|
end
|
|
tex.draw_texture(self:id(DIR .. "bg_outline_upper"))
|
|
if showing_highlight then
|
|
local params = { frame = red - 1, index = red - 1, fade = highlight }
|
|
if red > 9 then
|
|
params.frame = TILES - red
|
|
params.mirror = "horizontal"
|
|
end
|
|
tex.draw_texture(self:id(DIR .. "highlight_tile_upper"), params)
|
|
end
|
|
|
|
if self.final and self.final.won and since_end then
|
|
local a = math.min(math.max(since_end - 18, 0) / 26, 1)
|
|
self:spr("final_glow_lower", 640, 628, { fade = a })
|
|
self:spr("final_glow_upper", 640, 94, { fade = a })
|
|
self:spr("final_wreath", 640, 110, { fade = a, add = true })
|
|
local h = since_end - 18
|
|
if h >= 0 and h <= 26 then
|
|
self:spr("final_wreath_flash", 640, 110, { s = 1 + 1.5 * h / 26, fade = 1 - h / 26 })
|
|
end
|
|
end
|
|
|
|
local intro = (ms - self.start_ms) / FRAME_MS
|
|
if intro < 60 then
|
|
for side = 1, 2 do
|
|
local base = side == 1 and 151 or 1129
|
|
for i = 0, 11 do
|
|
local h = intro - i * 2
|
|
if h >= 0 and h < 24 then
|
|
self:spr("trail_quad", base + (side == 1 and 1 or -1) * (i - 6) * 42, 106 - 8 * ((i % 4) - 2), {
|
|
frame = i, fade = 0.4 * (1 - h / 24), mirror = side == 2 and "horizontal" or nil,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_front(ms, section, section_count, progress)
|
|
local scale = tex.get_screen_scale()
|
|
local r, g, b, a = 0, 255, 0, 255
|
|
if progress >= 0.75 then
|
|
r, g, b, a = 253, 249, 0, 255 * flash(ms, 266)
|
|
end
|
|
tex.draw_texture(self:id(DIR .. "progress_bar"))
|
|
tex.draw_rect(PROGRESS.x * scale, PROGRESS.y * scale, PROGRESS.w * scale, PROGRESS.h * scale, 95, 94, 95, 255)
|
|
tex.draw_rect(PROGRESS.x * scale, PROGRESS.y * scale, progress * PROGRESS.w * scale, PROGRESS.h * scale, r, g, b, a)
|
|
|
|
if section < section_count then
|
|
tex.draw_texture(self:id(DIR .. "section_text"), { index = 0, frame = section })
|
|
end
|
|
if section < section_count - 1 then
|
|
tex.draw_texture(self:id(DIR .. "section_text"), { index = 1, frame = section + 1 })
|
|
end
|
|
self.section, self.section_count = section, section_count
|
|
if section == section_count - 1 and not self.final then
|
|
local won, lost = 0, 0
|
|
for i = 0, section - 1 do
|
|
if self.wins[i] == true then won = won + 1 elseif self.wins[i] == false then lost = lost + 1 end
|
|
end
|
|
self:cue(won > lost and "vo_reach_win" or (won < lost and "vo_reach_lose" or "vo_reach_draw"), "voice", 0)
|
|
end
|
|
-- 3 section songs use only the right three slots of the 5 slot row
|
|
local slot_base = section_count == 3 and 2 or 0
|
|
tex.draw_texture(self:id(DIR .. (section_count == 3 and "sections_three" or "sections")))
|
|
if section < section_count then
|
|
tex.draw_texture(self:id(DIR .. "section_highlight_green"), { index = slot_base + section })
|
|
tex.draw_texture(self:id(DIR .. "section_highlight_white"), { index = slot_base + section, fade = flash(ms, 700) })
|
|
end
|
|
for i = 0, SECTIONS - 1 do
|
|
if self.wins[i] ~= nil then
|
|
tex.draw_texture(self:id(DIR .. (self.wins[i] and "section_win" or "section_lose")), { index = slot_base + i })
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Mid-height x of the territory border at each red tile count; the upper band's tiles fan out, so not even.
|
|
local BORDER_X = { 135, 193, 256, 316, 376, 436, 495, 553, 612, 667, 722, 783, 842, 903, 963, 1022, 1083, 1142 }
|
|
|
|
-- Each side's flame grows through four tiers. A tier's clock restarts when it is entered, so the opening
|
|
-- flare plays once and then only the part's loop repeats. x and y in a layer are the image's centre,
|
|
-- measured from the flame's origin with +x towards the opponent. { key, from frame, to frame, ... }.
|
|
local function fire_tier_2(first, second, sparks, spark_x, wisps, wisps_x)
|
|
return {
|
|
intro = 20, loop = 28,
|
|
{ first, 0, 19, sx = { 0.1, 1.2 }, x = { 93, -16 } },
|
|
{ first, 20, 26, sx = { 0.8, 1 }, x = { 26, 5 } },
|
|
{ second, 27, 33, sx = { 0.8, 1 }, x = { 26, 5 } },
|
|
{ first, 34, 40, sx = { 0.8, 1 }, x = { 26, 5 } },
|
|
{ second, 41, 47, sx = { 0.8, 1 }, x = { 26, 5 } },
|
|
{ sparks, 0, 14, sx = { 0.05, 0.6 }, x = { spark_x, -14 } },
|
|
{ sparks, 15, 19, sx = 0.6, x = { -23, -59 }, a = { 0.8, 0 } },
|
|
{ wisps, 20, 41, sx = { 0.2, 1.5 }, x = wisps_x, y = 1, a = { 1, 0 } },
|
|
}
|
|
end
|
|
|
|
local function small_horn(key, x, y, flip)
|
|
return {
|
|
start = 20, loop = 14, x = x, y = y, flip = flip,
|
|
{ key, 0, 9, s = { 1, 1.19 }, rot = { 0, -8 }, x = { -2, -26 }, y = { 0, -19 }, a = { 1, 0 }, n = 2 },
|
|
}
|
|
end
|
|
|
|
local PLAYER_HORN = {
|
|
loop = 15, normal = true,
|
|
{ "player_horn", 0, 9, sx = { 0.69, 0.88 }, x = { 137, 125 }, y = { -58, -69 }, a = { 0.28, 0.6 }, n = 2 },
|
|
{ "player_horn", 10, 14, sx = { 0.9, 1 }, x = { 123, 117 }, y = { -71, -77 }, a = { 0.48, 0 }, n = 2 },
|
|
}
|
|
local AI_HORN = {
|
|
loop = 20, normal = true,
|
|
{ "ai_horn", 0, 14, sx = { 0.44, 0.85 }, x = { 151, 126 }, y = { -58, -64 }, a = { 0.28, 0.6 }, n = 2 },
|
|
{ "ai_horn", 15, 19, sx = { 0.88, 1 }, x = { 125, 118 }, y = { -67, -78 }, a = { 0.48, 0 }, n = 2 },
|
|
}
|
|
|
|
local function placed(part, fields)
|
|
local placement = setmetatable(fields, { __index = part })
|
|
for i, layer in ipairs(part) do placement[i] = layer end
|
|
return placement
|
|
end
|
|
|
|
-- The opening stream of sparks and the big flame's flare are the same on both sides at the top tier.
|
|
local function fire_tier_4(large, burst, sparks, n, opening_burst)
|
|
local part = {
|
|
intro = 20, loop = 20,
|
|
{ burst, 20, 24, s = { 1, 1.2 }, x = { 19, -14 }, n = n },
|
|
{ burst, 30, 34, sx = { 0.8, 1.12 }, sy = { 1.2, 1.39 }, x = { 49, -3 }, a = { 1, 0.2 }, n = n },
|
|
{ large, 25, 29, s = { 1, 1.2 }, x = { 16, -16 }, n = n },
|
|
{ large, 35, 39, sx = { 0.6, 1 }, sy = 1.2, x = { 82, 16 }, n = n },
|
|
{ large, 0, 4, sx = { 0.1, 1 }, x = { 162, 27 }, n = n },
|
|
{ large, 5, 14, s = { 1.02, 1.2 }, x = { 24, -4 }, n = n },
|
|
{ large, 15, 19, sx = { 1.22, 1.29 }, sy = 1.2, x = { -7, -20 }, a = { 0.8, 0 }, n = n },
|
|
{ sparks, 0, 4, sx = { 0.1, 1 }, x = { 156, 27 } },
|
|
{ sparks, 5, 14, x = { 16, -83 } },
|
|
{ sparks, 15, 19, sx = { 1.02, 1.08 }, x = { -88, -105 }, a = { 0.8, 0 } },
|
|
}
|
|
if opening_burst then
|
|
part[#part + 1] = { burst, 0, 19, x = 19, n = n }
|
|
end
|
|
return part
|
|
end
|
|
|
|
local function spark_flash(key)
|
|
return {
|
|
loop = 60,
|
|
{ key, 0, 4, sx = { 0.1, 1 }, x = { 156, 0 } },
|
|
{ key, 5, 9, s = { 1.03, 1.13 }, x = { -24, -122 } },
|
|
{ key, 10, 14, s = { 1.15, 1.2 }, x = { -134, -183 }, a = { 0.8, 0 } },
|
|
}
|
|
end
|
|
|
|
local PLAYER_STREAK = {
|
|
loop = 20, normal = true,
|
|
{ "player_streak", 1, 5, sx = { 0.28, 1 }, sy = { 0.47, 1 }, x = { 125, 0 }, y = { -23, 0 } },
|
|
{ "player_streak", 6, 12, sx = { 1.03, 1.2 }, x = { -15, -106 }, y = 1 },
|
|
{ "player_streak", 13, 19, sx = 1.2, x = { -112, -149 }, y = 1, a = { 0.86, 0 } },
|
|
}
|
|
local AI_STREAK = {
|
|
loop = 20,
|
|
{ "ai_streak", 0, 5, sx = { 0.28, 1 }, sy = { 0.47, 1 }, x = { 88, -132 }, y = { -27, -8 } },
|
|
{ "ai_streak", 6, 11, sx = { 1.03, 1.17 }, x = { -138, -171 }, y = -8 },
|
|
{ "ai_streak", 12, 19, sx = 1.2, x = { -178, -193 }, y = -7, a = { 1, 0 } },
|
|
}
|
|
|
|
local PLAYER_FLAMES = {
|
|
{ x = -32, y = 101, {
|
|
loop = 19,
|
|
{ "player_small", 0, 4, s = { 0.9, 1 }, x = { -46, -53 }, frame = 0 },
|
|
{ "player_mid_1", 5, 10, sx = { 0.54, 0.62 }, sy = { 1, 1.15 }, x = -60 },
|
|
{ "player_small", 11, 14, x = -54.5, frame = 1 },
|
|
{ "player_small_end", 15, 18 },
|
|
} },
|
|
{ x = -134, y = 101, small_horn("player_horn_small", 67, -41),
|
|
small_horn("player_horn_small", 67, 49, true),
|
|
fire_tier_2("player_mid_2", "player_mid_1", "player_sparks", 96, "player_wisps", { 81, -48 }) },
|
|
{ x = -211, y = 91, placed(PLAYER_HORN, { x = -35, y = 8, s = 1.2, normal = false }),
|
|
placed(PLAYER_HORN, { x = -43, y = 21, s = 1.2, flip = true, normal = false }), {
|
|
intro = 20, loop = 20, x = -35, s = 1.2,
|
|
{ "player_medium_1", 20, 23, sx = { 0.6, 0.68 }, x = { 79, 67 } },
|
|
{ "player_medium_1", 24, 25, sx = { 0.7, 0.5 }, x = 63, a = { 1, 0 } },
|
|
{ "player_medium_1", 30, 33, sx = { 0.6, 0.68 }, x = { 79, 67 } },
|
|
{ "player_medium_1", 34, 35, sx = { 0.7, 0.5 }, x = 63, a = { 1, 0 } },
|
|
{ "player_medium_2", 4, 16, sx = { 1, 1.14 }, x = { 16, -4 } },
|
|
{ "player_medium_2", 17, 19, sx = { 1.16, 1.19 }, x = { -7, -13 }, a = { 0.67, 0 } },
|
|
{ "player_medium_2", 25, 29, sx = { 0.6, 0.7 }, x = { 80, 63 } },
|
|
{ "player_medium_2", 35, 39, sx = { 0.6, 0.7 }, x = { 80, 63 } },
|
|
{ "player_medium_2", 0, 16, sx = { 0.1, 1.14 }, x = { 162, -4 }, ease = true },
|
|
{ "player_medium_2", 17, 19, sx = { 1.16, 1.19 }, x = { -7, -13 }, a = { 0.67, 0 } },
|
|
{ "player_wisps_wide", 20, 29, sx = { 0.09, 1.24 }, x = { 165, -15 }, y = 10, a = { 1, 0 } },
|
|
{ "player_sparks", 0, 4, sx = { 0.1, 1 }, x = { 156, 16 } },
|
|
{ "player_sparks", 5, 16, x = { 7, -94 } },
|
|
{ "player_sparks", 17, 19, sx = { 1.02, 1.05 }, x = { -99, -107 }, a = { 0.67, 0 } },
|
|
} },
|
|
{ x = -210, y = 91,
|
|
placed(PLAYER_STREAK, { x = -18, y = 84 }), placed(PLAYER_STREAK, { x = -19, y = -61, flip = true }),
|
|
placed(PLAYER_HORN, { x = -29, y = 10, s = 1.2 }),
|
|
placed(PLAYER_HORN, { x = -29, y = 18, sx = 1.2, sy = 1.08, flip = true }),
|
|
fire_tier_4("player_large", "player_burst", "player_sparks", 1), spark_flash("player_sparks") },
|
|
}
|
|
|
|
local AI_FLAMES = {
|
|
{ x = 41, y = 101, {
|
|
loop = 19,
|
|
{ "ai_small", 11, 14, x = -50.5, frame = 2 },
|
|
{ "ai_small", 0, 4, s = { 0.9, 1 }, x = { -46, -53 }, frame = 0, n = 3 },
|
|
{ "ai_small", 5, 10, sx = { 0.54, 0.62 }, sy = { 1, 1.15 }, x = { -29, -37 }, frame = 1, n = 3 },
|
|
{ "ai_small_end", 15, 18, n = 3 },
|
|
} },
|
|
{ x = -63, y = 101, small_horn("ai_horn_small", 66, -41),
|
|
small_horn("ai_horn_small", 66, 49, true),
|
|
fire_tier_2("ai_mid_1", "ai_mid_2", "ai_sparks", 94, "ai_wisps", { 84, 24 }) },
|
|
{ x = -138, y = 91, placed(AI_HORN, { x = -33, y = 8, s = 1.2 }),
|
|
placed(AI_HORN, { x = -33, y = 20, s = 1.2, flip = true }), {
|
|
intro = 20, loop = 20, x = -38, s = 1.2,
|
|
{ "ai_medium_1", 20, 23, sx = { 0.6, 0.68 }, x = { 83, 70 } },
|
|
{ "ai_medium_1", 24, 25, sx = { 0.7, 0.5 }, x = { 66, 63 }, a = { 1, 0 } },
|
|
{ "ai_medium_1", 30, 33, sx = { 0.6, 0.68 }, x = { 83, 70 } },
|
|
{ "ai_medium_1", 34, 35, sx = { 0.7, 0.5 }, x = { 66, 63 }, a = { 1, 0 } },
|
|
{ "ai_medium_2", 0, 16, sx = { 0.1, 1.14 }, x = { 162, -4 }, ease = true },
|
|
{ "ai_medium_2", 17, 19, sx = { 1.16, 1.19 }, x = { -7, -13 }, a = { 0.67, 0 } },
|
|
{ "ai_medium_2", 25, 29, sx = { 0.6, 0.7 }, x = { 82, 67 } },
|
|
{ "ai_medium_2", 35, 39, sx = { 0.6, 0.7 }, x = { 82, 67 } },
|
|
{ "player_sparks", 0, 4, sx = { 0.1, 1 }, x = { 156, 16 } },
|
|
{ "player_sparks", 5, 16, x = { 7, -94 } },
|
|
{ "player_sparks", 17, 19, sx = { 1.02, 1.05 }, x = { -99, -107 }, a = { 0.67, 0 } },
|
|
{ "ai_wisps_wide", 20, 34, sx = { 0.09, 1.24 }, x = { 169, 33 }, y = 10, a = { 1, 0 } },
|
|
} },
|
|
{ x = -145, y = 91,
|
|
placed(AI_STREAK, { x = 45, y = 81 }), placed(AI_STREAK, { x = 46, y = -59, flip = true }),
|
|
placed(AI_HORN, { x = -29, y = 18, s = 1.2 }),
|
|
placed(AI_HORN, { x = -29, y = 18, sx = 1.2, sy = 1.08, flip = true }),
|
|
fire_tier_4("ai_large", "ai_burst", "ai_sparks", 2, true), spark_flash("ai_sparks") },
|
|
}
|
|
|
|
-- The player's section-winning shot: one flare, not a whole flame tier.
|
|
local PLAYER_SHOT = {
|
|
{ "player_large", 0, 16, sx = { 0.1, 1.3 }, x = { 145, -54 }, ease = true },
|
|
{ "player_large", 17, 19, sx = { 1.33, 1.4 }, x = { -60, -72 }, a = { 0.67, 0 } },
|
|
{ "player_sparks", 0, 4, sx = { 0.1, 1 }, x = { 139, 0 } },
|
|
{ "player_sparks", 5, 14, x = { -12, -115 } },
|
|
{ "player_sparks", 15, 19, x = { -127, -173 }, a = { 0.8, 0 } },
|
|
}
|
|
|
|
local function player_flame_tier(position)
|
|
if position <= 3 then return 1 end
|
|
if position <= 7 then return 2 end
|
|
return position <= 13 and 3 or 4
|
|
end
|
|
|
|
local function ai_flame_tier(position)
|
|
if position <= 2 then return 4 end
|
|
if position <= 10 then return 3 end
|
|
return position <= 14 and 2 or 1
|
|
end
|
|
|
|
local function sweep(value, p, default)
|
|
if type(value) == "table" then return value[1] + (value[2] - value[1]) * p end
|
|
return value or default
|
|
end
|
|
|
|
local function part_clock(part, f)
|
|
f = f - (part.start or 0)
|
|
if f < 0 then return nil end
|
|
local intro = part.intro or 0
|
|
if not part.loop or f < intro then return f end
|
|
return intro + (f - intro) % part.loop
|
|
end
|
|
|
|
function AIBattleOverlay:draw_flame_part(part, f, origin, y, dir, alpha)
|
|
f = part_clock(part, f)
|
|
if not f then return end
|
|
local px, py = part.x or 0, part.y or 0
|
|
local psx, psy = part.sx or part.s or 1, part.sy or part.s or 1
|
|
local ydir = part.flip and -1 or 1
|
|
for _, l in ipairs(part) do
|
|
if f >= l[2] and f < l[3] + 1 then
|
|
local p = l[3] > l[2] and math.min((f - l[2]) / (l[3] - l[2]), 1) or 1
|
|
if l.ease then p = 1 - (1 - p) * (1 - p) end
|
|
local rot = sweep(l.rot, p, 0)
|
|
local mirror
|
|
if dir < 0 and part.flip then
|
|
rot = rot + 180
|
|
elseif dir < 0 then
|
|
mirror, rot = "horizontal", -rot
|
|
elseif part.flip then
|
|
mirror, rot = "vertical", -rot
|
|
end
|
|
for _ = 1, l.n or 1 do
|
|
self:spr("flame_" .. l[1], origin + dir * (px + psx * sweep(l.x, p, 0)),
|
|
y + py + ydir * psy * sweep(l.y, p, 0), {
|
|
sx = sweep(l.sx or l.s, p, 1) * psx, sy = sweep(l.sy or l.s, p, 1) * psy,
|
|
rot = rot, frame = l.frame, fade = sweep(l.a, p, 1) * alpha,
|
|
mirror = mirror, add = not part.normal,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_flames(ms, border, position, dir)
|
|
local tier = dir > 0 and player_flame_tier(position) or ai_flame_tier(position)
|
|
local entered = self.flame_tier[dir]
|
|
if not entered or entered.tier ~= tier then
|
|
entered = { tier = tier, start = ms }
|
|
self.flame_tier[dir] = entered
|
|
end
|
|
local spec = (dir > 0 and PLAYER_FLAMES or AI_FLAMES)[tier]
|
|
local f = (ms - entered.start) / FRAME_MS
|
|
for _, part in ipairs(spec) do
|
|
self:draw_flame_part(part, f, border + dir * spec.x, spec.y, dir, 1)
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:border_x(territory)
|
|
return BORDER_X[math.min(math.max(1, territory + 10), TILES - 1)]
|
|
end
|
|
|
|
function AIBattleOverlay:draw_blast(f, border, dir)
|
|
if f < 23 or f >= 32 then return end
|
|
local p = math.min((f - 23) / 5, 1)
|
|
local alpha = f < 29 and 1 or 0.67 * (1 - (f - 29) / 3)
|
|
if dir > 0 then
|
|
local x = -41 + (border - 140 + 41) * p
|
|
self:draw_flame_part(PLAYER_SHOT, f - 23, x, 81, 1, alpha)
|
|
else
|
|
local x = 1164 + (border + 110 - 1164) * p
|
|
for _, part in ipairs(AI_FLAMES[4]) do
|
|
self:draw_flame_part(part, f - 23, x, 101, -1, alpha)
|
|
end
|
|
end
|
|
end
|
|
|
|
local FLAME_Y = 100
|
|
|
|
function AIBattleOverlay:draw_impact(f, x)
|
|
local h = f - 31
|
|
if h < 0 or h > 60 then return end
|
|
local p = h / 60
|
|
self:spr("burst_lightning", x, FLAME_Y, { s = 0.8 + 0.5 * math.min(h / 11, 1), fade = 1 - math.min(h / 11, 1), add = true })
|
|
self:spr("burst_rays", x, FLAME_Y, { s = 0.1 + 0.9 * p, rot = 120 * p, fade = 0.63 * (1 - p), add = true })
|
|
for i = 0, 3 do
|
|
local a = i * math.pi / 2 + 0.7
|
|
self:spr("burst_star", x + math.cos(a) * 130 * p, FLAME_Y + math.sin(a) * 90 * p, {
|
|
s = 1 - p, fade = 1 - p, add = true,
|
|
})
|
|
end
|
|
end
|
|
|
|
local SPARKS = {
|
|
{ "spark_yellow_small", -70, 53, 50, -60, 90 }, { "spark_yellow", -87, -43, -60, -133, 80 },
|
|
{ "spark_purple", -165, -85, -132, -50, 90 }, { "spark_pink_small", -66, -20, 30, 40, 80 },
|
|
{ "spark_white", -82, 103, 40, 60, 80 },
|
|
}
|
|
|
|
function AIBattleOverlay:draw_sparks(f, x, y, scale)
|
|
for _, s in ipairs(SPARKS) do
|
|
if f >= 0 and f <= s[6] then
|
|
local p = f / s[6]
|
|
self:spr(s[1], x + (s[2] + (s[4] - s[2]) * p) * scale, y + (s[3] + (s[5] - s[3]) * p) * scale, {
|
|
s = scale * (1.2 - 0.9 * p), rot = 360 * p, fade = math.min(p * 4, 1 - p), add = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_rising(f)
|
|
if f < 0 or f > 180 then return end
|
|
local p = f / 179
|
|
local fade = math.min(1, (1 - p) * 4)
|
|
self:spr("rising_emblems_1", 209, 859 - 259 * p, { fade = fade, add = true })
|
|
self:spr("rising_emblems_2", 1039, 855 - 256 * p, { fade = fade, add = true })
|
|
self:spr("rising_emblems_1", 209, 227 - 154 * p, { fade = fade, add = true, mirror = "horizontal" })
|
|
self:spr("rising_emblems_2", 1039, 296 - 196 * p, { fade = fade, add = true, mirror = "horizontal" })
|
|
for _, spot in ipairs({ { 524, 625 }, { 948, 120 }, { 448, 120 } }) do
|
|
for i = 0, 2 do
|
|
local a = i * 2.1 + spot[1]
|
|
self:spr("rising_triangle", spot[1] + math.cos(a) * 95 * p, spot[2] + math.sin(a) * 60 * p, {
|
|
s = 0.75 - 0.5 * p, rot = 360 * p, fade = fade, add = true,
|
|
})
|
|
end
|
|
self:spr("rising_outline", spot[1], spot[2], { s = 0.5, rot = 360 * p, fade = fade, add = true })
|
|
end
|
|
self:draw_sparks(f % 100, 924, 574, 0.8)
|
|
self:draw_sparks(f % 100, 355, 574, 0.8)
|
|
end
|
|
|
|
function AIBattleOverlay:draw_clash(ms, territory, angry)
|
|
local border = self:border_x(territory)
|
|
local active = not self.final and not self:event_frame(ms)
|
|
if active then
|
|
self:draw_flames(ms, border, territory + 9, 1)
|
|
self:draw_flames(ms, border, territory + 9, -1)
|
|
else
|
|
self.flame_tier = {}
|
|
end
|
|
local event_f = self:event_frame(ms)
|
|
if event_f and not self.final then
|
|
self:draw_blast(event_f, border, self.event.won and 1 or -1)
|
|
self:draw_impact(event_f, self.event.won and 1200 or 80)
|
|
if self.event.won then self:draw_rising(event_f) end
|
|
elseif self.final and self.final.start and self.final.won then
|
|
self:draw_rising((ms - self.final.start) / FRAME_MS)
|
|
end
|
|
if angry and not self.final then
|
|
local f = (ms / FRAME_MS) % 20
|
|
local x = border + 79
|
|
self:spr("emotion_angry", x, 50, { s = 1 + 0.1 * flash(f, 20) })
|
|
if f >= 9 then
|
|
local h = (f - 9) / 10
|
|
self:spr("emotion_angry_flash", x, 50, { s = 1.1 + 0.4 * h, fade = 1 - h })
|
|
end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_section_result(ms)
|
|
local event = self.event
|
|
if not event or not event.start then return end
|
|
local f = (ms - event.start) / FRAME_MS - 4
|
|
if f < 0 or f > 104 then return end
|
|
local key = event.won and "result_win" or "result_lose"
|
|
local CX, CY = 403, 93
|
|
if f <= 95 then
|
|
local frame = 0
|
|
if f >= 55 and f < 75 then frame = math.min(1 + (f - 55) // 4, 5) end
|
|
if f < 75 then
|
|
self:spr(key, CX, CY, { s = track(f, { { 0, 2.714 }, { 16, 1 } }), frame = math.floor(frame) })
|
|
else
|
|
local h = (f - 75) / 9
|
|
self:spr(key, CX, CY)
|
|
if h <= 1 then
|
|
self:spr(key .. "_flash", CX, CY, { s = 1 + 2 * h, fade = 1 - h, add = true })
|
|
end
|
|
end
|
|
else
|
|
local h = (f - 96) / 8
|
|
self:spr(key, CX - 120 * h, CY, { s = 1 - 0.7 * h, fade = 1 - h })
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_section_pops(ms)
|
|
local event = self.event
|
|
if not event or not event.start or not event.won then return end
|
|
local h = ((ms - event.start) / FRAME_MS - 104) / 12
|
|
if h < 0 or h > 1 then return end
|
|
local slot = (self.section_count == 3 and 2 or 0) + event.section
|
|
local x = 190 + slot * 31
|
|
local y = slot % 2 == 0 and 111 or 126
|
|
self:spr("section_pop", x, y, { s = 0.8 + 0.6 * h, fade = 1 - h, add = true })
|
|
end
|
|
|
|
function AIBattleOverlay:draw_circuit(f)
|
|
if f > 68 then return end
|
|
local fade = f < 50 and 1 or (68 - f) / 18
|
|
for side = 1, 2 do
|
|
local dir = side == 1 and 1 or -1
|
|
local mirror = side == 2 and "horizontal" or nil
|
|
local x0 = side == 1 and 200 or 1077
|
|
local y0 = side == 1 and 376 or 336
|
|
self:spr("finish_circuit", x0 + dir * 94, y0, { mirror = mirror, fade = fade * 0.9 })
|
|
for i, streak in ipairs({ { 3, 434, 200, -15 }, { 23, 411, 330, -121 }, { 35, 308, 100, -172 } }) do
|
|
local h = (f - streak[1]) / 20
|
|
if h >= 0 and h <= 1 then
|
|
self:spr("finish_streak", x0 + dir * (streak[2] + (streak[3] - streak[2]) * h), y0 + dir * streak[4], {
|
|
sx = 2.5 * (1 - h) + 0.2, fade = fade, mirror = mirror, add = true,
|
|
})
|
|
self:spr("finish_dot", x0 + dir * (streak[2] + (streak[3] - streak[2]) * h), y0 + dir * streak[4], {
|
|
s = 1 + h, fade = fade * (1 - h), add = true,
|
|
})
|
|
end
|
|
if i == 3 and f >= 46 then
|
|
self:spr("finish_dot", x0 + dir * 12, y0 - dir * 33, {
|
|
s = 1 + (f - 46) / 20, fade = fade * (1 - (f - 46) / 20), add = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function dim(alpha)
|
|
tex.draw_rect(0, 0, 1280 * S, 720 * S, 0, 0, 0, 255 * alpha)
|
|
end
|
|
|
|
function AIBattleOverlay:draw_finish(f)
|
|
local CX, CY = 640, 360
|
|
dim(math.min(f / 124, 1) * 0.5)
|
|
self:draw_circuit(f)
|
|
local h = f - 4
|
|
if h >= 0 and h <= 40 then
|
|
local p = h / 40
|
|
self:spr("finish_ring_flash", CX, CY, { s = 0.1 + 1.9 * p, rot = 270 * p, fade = 1 - p * p })
|
|
end
|
|
if h >= 0 and h <= 8 then
|
|
self:spr("finish_disc", CX, CY, { s = 0.5 + 1.136 * h / 8, fade = 1 - h / 8 })
|
|
end
|
|
if f <= 12 then
|
|
self:spr("finish_dot", CX, CY, { s = 1 + 10 * f / 12, fade = 1 - f / 12 })
|
|
end
|
|
local g = f - 27 -- ring, disc and logo arrive
|
|
if g < 0 then return end
|
|
local glow_phase = (f % 400) / 400
|
|
self:spr("finish_glow", CX, CY, {
|
|
s = 1 + 0.2 * flash(f, 120), fade = math.min(g / 60, 1) * track(f % 400, { { 0, 0 }, { 75, 0.2 }, { 120, 0 }, { 400, 0 } }),
|
|
})
|
|
local settle = math.min(g / 96, 1)
|
|
self:spr("finish_ring_dashed", CX, CY, {
|
|
s = 0.1 + 0.9 * math.min(g / 40, 1), rot = (f < 125 and -90 * (1 - settle) or 360 * glow_phase),
|
|
})
|
|
if f >= 37 then
|
|
self:spr("finish_ring", CX, CY, {
|
|
s = track(f - 37, { { 0, 0.4 }, { 40, 1 } }), rot = (f < 125 and 80 * (1 - settle) or -360 * glow_phase),
|
|
})
|
|
end
|
|
if g <= 20 then
|
|
self:spr("finish_disc", CX, CY, { s = track(g, { { 0, 0.7 }, { 12, 1.152 }, { 20, 0.85 } }) })
|
|
elseif f <= 41 + 27 then
|
|
self:spr("finish_disc", CX, CY, { s = 0.85 + 0.95 * (g - 20) / 21, fade = 1 - (g - 20) / 21 })
|
|
end
|
|
local pulse = f < 125 and 1 or 1 - 0.05 * flash(f, 60)
|
|
self:spr("finish_logo", CX, CY - 2, {
|
|
s = track(f - 27, { { 0, 0.8 }, { 14, 1.2 }, { 30, 1 } }) * pulse, fade = math.min(g / 14, 1),
|
|
})
|
|
end
|
|
|
|
function AIBattleOverlay:draw_win(f)
|
|
local LAUREL_X, LAUREL_Y = 634, 351
|
|
local BATTLE_X, BATTLE_Y = 628, 264
|
|
local TEXT_X, TEXT_Y = 631, 430
|
|
local grow = track(f, { { 0, 3 }, { 20, 1 } })
|
|
self:spr("win_laurel", LAUREL_X, LAUREL_Y, { s = grow, fade = math.min(f / 20, 1) })
|
|
local flash_f = (f - 42)
|
|
if flash_f >= 0 and flash_f <= 40 then
|
|
self:spr("win_laurel_flash", LAUREL_X, LAUREL_Y, { s = 1 + flash_f / 40, fade = 1 - flash_f / 40, add = true })
|
|
elseif f > 149 and (f - 149) % 128 >= 64 then
|
|
self:spr("win_laurel_flash", LAUREL_X, LAUREL_Y, { fade = blink(((f - 149) % 128) - 64, 64), add = true })
|
|
end
|
|
local ray = f - 40
|
|
if ray >= 0 and ray <= 90 then
|
|
local p = ray / 90
|
|
self:spr("fc_rays", LAUREL_X, LAUREL_Y + 76, { rot = -90 * p, fade = 0.63 * (1 - p), add = true })
|
|
self:spr("fc_rays", LAUREL_X, LAUREL_Y + 76, { rot = 90 * p, fade = 0.63 * (1 - p), add = true })
|
|
end
|
|
if f >= 20 then
|
|
self:spr("win_text", TEXT_X, TEXT_Y, { s = track(f, { { 20, 3 }, { 40, 1 } }), fade = math.min((f - 20) / 20, 1) })
|
|
self:spr("win_battle_text", BATTLE_X, BATTLE_Y, { fade = math.min((f - 20) / 5, 1) })
|
|
end
|
|
if f >= 60 then
|
|
self:spr("win_shadow", TEXT_X + 1, TEXT_Y, { fade = track(f, { { 60, 0.8 }, { 72, 0 } }) })
|
|
end
|
|
for i, b in ipairs({ { 343, -175 }, { 444, 159 }, { 66, 507 }, { -571, -163 }, { -778, 150 }, { -493, 488 } }) do
|
|
self:draw_sparks(f - 42 - (i % 3) * 5, 803 + b[1], 270 + b[2], 0.87)
|
|
end
|
|
if f >= 73 then
|
|
for i, star in ipairs({ { 638, 346 }, { 963, 400 }, { 613, 330 }, { 650, 250 } }) do
|
|
local a = ((f - 73 + i * 14) % 75) / 40
|
|
if a <= 1 then
|
|
self:spr("win_star", star[1], star[2], {
|
|
s = track(a, { { 0, 0.6 }, { 1, 1.3 } }), fade = blink(a, 1), add = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_full_combo(f)
|
|
local CX, CY = 640, 360
|
|
local loop = f <= 250 and f or 130 + (f - 130) % 120
|
|
self:spr("fc_glow", CX, CY - 20, { fade = track(loop, { { 0, 0 }, { 129, 0.99 }, { 130, 0.99 } }), add = true })
|
|
if loop >= 40 then
|
|
local p = math.min((loop - 40) / 90, 1)
|
|
self:spr("fc_rays", CX, CY, { rot = -180 * p, fade = 0.63 * (1 - p), add = true })
|
|
self:spr("fc_rays", CX, CY, { rot = 180 * p, fade = 0.63 * (1 - p), add = true })
|
|
end
|
|
if f >= 22 then
|
|
local h = math.min((f - 22) / 48, 1)
|
|
self:spr("fc_emblem", CX, CY, { s = 0.1 + 1.1 * h, fade = 1 - h })
|
|
end
|
|
local text_scale = track(f, { { 0, 3 }, { 40, 1 } })
|
|
self:spr("fc_text", 636, 348, { s = text_scale, fade = math.min(f / 40, 1) })
|
|
if f >= 40 and f <= 70 then
|
|
local h = (f - 40) / 30
|
|
self:spr("fc_text_flash", 636, 348, { s = 1 + 2 * h, fade = 1 - h, add = true })
|
|
end
|
|
if loop >= 130 and loop <= 199 then
|
|
self:spr("fc_text_flash", 636, 348, { fade = 0.5 * blink(loop - 130, 69), add = true })
|
|
end
|
|
for i, b in ipairs({ { 1175, 73 }, { 1276, 407 }, { 898, 755 }, { 261, 85 }, { 54, 398 }, { 339, 736 } }) do
|
|
self:draw_sparks(loop - 42 - (i % 3) * 5, b[1], b[2], 0.87)
|
|
end
|
|
if loop >= 40 then
|
|
for i, star in ipairs({ { 440, 294 }, { 941, 384 }, { 604, 406 }, { 780, 284 } }) do
|
|
local a = ((loop - 40 + i * 14) % 69) / 40
|
|
if a <= 1 then self:spr("win_star", star[1], star[2], { s = 0.6 + 0.7 * a, fade = blink(a, 1), add = true }) end
|
|
end
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_finale(ms)
|
|
self:draw_section_result(ms)
|
|
self:draw_section_pops(ms)
|
|
local final = self.final
|
|
if not final or not final.start then return end
|
|
local f = (ms - final.start) / FRAME_MS
|
|
local start_length = final.won and 149 or 125
|
|
self:cue(final.won and "se_win_laurel" or "se_lose_beam", "sound", f)
|
|
self:cue(final.won and "vo_last_win" or "vo_last_lose", "voice", f)
|
|
self:cue(final.won and "se_win_logo" or "se_lose_logo", "sound", f, final.won and 25 or 4)
|
|
if final.full_combo then
|
|
self:cue("se_full_combo_logo", "sound", f, start_length + 22)
|
|
self:cue("se_full_combo_spark", "sound", f, start_length + 40)
|
|
end
|
|
if final.full_combo and f >= start_length then
|
|
self:draw_full_combo(f - start_length)
|
|
elseif final.won then
|
|
self:draw_win(f)
|
|
else
|
|
self:draw_finish(f)
|
|
end
|
|
end
|
|
|
|
function AIBattleOverlay:draw_scoreboard(is_ai, good, ok, bad, drumroll)
|
|
if is_ai then tex.draw_texture(self:id(DIR .. "ai_plate")) end
|
|
tex.draw_texture(self:id(DIR .. (is_ai and "scoreboard_ai" or "scoreboard")))
|
|
local digit = self:id(DIR .. "scoreboard_num")
|
|
local margin = 8
|
|
for row, counter in ipairs({ good, ok, bad, drumroll }) do
|
|
local digits = tostring(counter)
|
|
local width = #digits * margin
|
|
for i = 1, #digits do
|
|
tex.draw_texture(digit, {
|
|
frame = tonumber(digits:sub(i, i)),
|
|
x = -(width // 2) + (i - 1) * margin,
|
|
index = (row - 1) + (is_ai and 4 or 0),
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
AIGauge = setmetatable({}, { __index = Gauge })
|
|
AIGauge.__index = AIGauge
|
|
|
|
function AIGauge.new(is_2p, dan, string_diff)
|
|
local opts = { folder = "gauge_ai", scale = GAUGE_SCALE, footer = true, no_border = true }
|
|
return setmetatable(Gauge.new(is_2p, dan, string_diff, opts), AIGauge)
|
|
end
|
|
|
|
function AIGauge:blit(t, mirror, x, y, ...)
|
|
return Gauge.blit(self, t, mirror, (x or 0) + GAUGE_X / GAUGE_SCALE, y + GAUGE_Y, ...)
|
|
end
|
|
|
|
function AIGauge:draw(_, ...)
|
|
return Gauge.draw(self, 0, ...)
|
|
end
|