Files
PyTaikoGreen/Scripts/game/enso_battle.lua
T
2026-11-15 00:24:05 -05:00

1153 lines
44 KiB
Lua

local SKILLS = require("game/enso_skills")
local STAGES = require("game/enso_stages")
local INTERCEPTS = require("game/enso_intercepts")
EnsoBattleOverlay = {}
EnsoBattleOverlay.__index = EnsoBattleOverlay
local function folder_of(key)
return key:sub(1, 6) == "stage_" and "battle_stage" or "enso_battle"
end
local S = tex.get_screen_scale()
local FIELD = { x0 = 660, x1 = 1220, y0 = 470, y1 = 660, near = 0.7, far = 0.5 }
local POP_STAGGER_MS = 120
local POP_MS, DEAD_MS, ESCAPE_MS = 550, 450, 500
local HIT_TILT_MS, HIT_SHAKE_MS, SPARK_MS = 67, 200, 200
local LANE_HIT_MS = 250
local NUMBER = { x = 414, y = 168, drop_from = -10, drop_to = 12, drop_ms = 67, life_ms = 300 }
local KILL_TICK_MS, KILL_TICK_CATCHUP = 250, 20
local KILL_ADD = { x = 450, cy = 92, gap = -8 }
local DIGIT_POP_MS = 67
local SPARK = { per_kill = 3, delay_ms = 250, fly_ms = 650, x = 470, y = 92, field_x0 = 480, field_x1 = 1100 }
local ROLL = { x = 425, y = 140, spread_x = 85, spread_y = 45, s = 0.6, bg_ms = 550, number_ms = 483, max = 14 }
local CUTIN_MS, CUTIN_IN, SLASH_AT, CUTIN_OUT = 2420, 450, 1167, 2117
local FRAME_MS = 1000 / 60
local SLASH = { x = -314, y = -75, s = 0.873, cx = 186, cy = 323 }
local CHEST_MS = 1900
local CHEST_COUNT = { x = 171.5, y = 260.4, badge_x = 192.5, badge_y = 234.4, orb_y = 266.4, delay = 1133 }
local BOSS_DMG_MS = 117
local BEST_BANNER = { x = 72, y = 163, x_off = -86, slide = 14, out = 136, len = 149 }
local BOSS_HIT = { x0 = 650, x1 = 1000, y0 = 380, y1 = 600, ms = 300, max = 6 }
local BOSS_KNOCK = { at = 1300, out_ms = 350, back = 767, land = 1183, settle = 1317, done = 1500 }
local FINISH = { delay = 1000, boss_dead = 0, x = 804, y = 260, seal_x = 1040, seal_y = 260, slash_dx = -285,
slash_dy = 15, shatter = 1517, shard_ms = 383, banner = 1833 }
local FC = { dx = -4, dy = -6.6, loop_from = 44, loop_len = 95, band_from = 39, band_period = 90 }
local SHARDS = {
{ -15, -139, -1, -244, 1.765 }, { -159, -49, -318, -158, 1.618 },
{ -105, 65, -148, 149, 1.618 }, { 67, -18, 241, -41, 1.882 },
}
local BOSS_GAUGE = { w = 440, warn = 1 / 3, critical = 0.1, warn_ms = 850, critical_ms = 583, bump_ms = 33 }
local KILL_DIGITS = { right = 440, cy = 90, gap = -6 }
local POWER_DIGITS = { right = 106, cy = 208, gap = -2 }
local GAUGE_FILL_WIDTH = 200
local function clamp01(v)
return math.max(0, math.min(1, v))
end
local function lerp(a, b, q)
return a + (b - a) * clamp01(q)
end
local function ease_out(t)
return 1 - (1 - t) * (1 - t)
end
-- 0 -> 1 over [a, b] ms, then 1 -> 0 over [c, d]
local function window(t, a, b, c, d)
if t < a or t >= d then return 0 end
if t < b then return (t - a) / (b - a) end
if t < c then return 1 end
return 1 - (t - c) / (d - c)
end
function EnsoBattleOverlay.new()
local self = setmetatable({}, EnsoBattleOverlay)
self.ids = {}
self.sizes = {}
self:reset(414 * S, 256 * S)
return self
end
function EnsoBattleOverlay:id(key)
self.ids[key] = self.ids[key] or tex.get_texture(folder_of(key) .. "/" .. key)
return self.ids[key]
end
function EnsoBattleOverlay:size(key)
local info = self.sizes[key]
if not info then
info = tex.get_texture_info(folder_of(key), key)
self.sizes[key] = info
end
return info.width * S, info.height * S
end
function EnsoBattleOverlay:spr(key, x, y, o)
o = o or {}
local w, h = self:size(key)
local sx, sy = o.sx or o.s or 1, o.sy or o.s or 1
tex.draw_texture(self:id(key), {
x = x * S, y = y * 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,
blend = o.add and "additive" or nil,
})
end
local function play(name, preset)
name = "enso_battle_" .. name
if audio.has_sound(name) then audio.play_sound(name, preset or "sound") end
end
function EnsoBattleOverlay:reset(judge_x, judge_y, stage, orbs, best_power)
self.judge = { x = judge_x / S, y = judge_y / S }
self.chest_count = { orbs = orbs, counts = {}, bumps = {} }
stage = STAGES[stage] and stage or 1
-- the EX stage (11) fields stage 9's enemies once its boss is gone
self.stage, self.enemy_stage = STAGES[stage], STAGES[math.min(stage - 1 + stage % 2, 9)]
self.intercept_def = INTERCEPTS[stage]
self.intercept = nil
self.enemies = {}
self.next_pop = 0
self.fallen = {}
self.number = nil
self.kill_add = nil
self.skill_add = nil
self.kill_count = { target = 0, shown = 0, next_tick = 0, pops = {}, incoming = {} }
self.sparks = {}
self.puff = nil
self.rolls = {}
self.power_count = { shown = 0, pops = {} }
self.lane_hits = {}
self.chests = {}
self.skill = nil
self.ended = nil
self.end_banner = nil
self.fight = nil
self.power, self.fill = 0, 0
self.best_power = best_power and best_power >= 0 and best_power or nil
self.best_banner = nil
end
function EnsoBattleOverlay:boss(ms, hp, max)
self.fight = { start = ms, hp = hp, shown = hp, from = hp, max = max, hits = {} }
self.fight.def = self.stage.boss or STAGES[2].boss
if self.intercept_def then
self.intercept = { start = ms, last = ms, gogo = false, active = {},
normal = { t = 0, i = 1 }, gogo_mode = { t = 0, i = 1 } }
end
end
function EnsoBattleOverlay:set_gogo(ms, on)
local ic = self.intercept
if not ic or ic.gogo == on then return end
ic.gogo = on
for _, a in ipairs(ic.active) do
if a.gogo ~= on and not a.out then a.out = ms end
end
end
function EnsoBattleOverlay:cue_once(key, name)
local played = self.intercept_played
if played and not played[key] then
played[key] = true
play(name)
end
end
function EnsoBattleOverlay:draw_intercept(ms)
local ic = self.intercept
if not ic then return end
if self.ended and not ic.over then
ic.over = true
for _, a in ipairs(ic.active) do a.out = a.out or ms end
end
if not ic.over then
local mode = ic.gogo and ic.gogo_mode or ic.normal
local def = ic.gogo and self.intercept_def.gogo or self.intercept_def.normal
mode.t = mode.t + (ms - ic.last) / FRAME_MS
local ev = def.events[mode.i]
if mode.t >= ev[1] then
if ev[2] > 0 then
ic.active[#ic.active + 1] = {
start = ms, gogo = ic.gogo, pat = def.pats[ev[2]], def = def, played = {},
}
end
for _, name in ipairs(def.sounds[mode.i] or {}) do
play(name, name:sub(1, 3) == "vo_" and "voice" or nil)
end
mode.i = mode.i + 1
if mode.i > #def.events then mode.t, mode.i = 0, 1 end
end
end
ic.last = ms
local always = self.intercept_def.always
if always then
local fade = ic.over and math.max(0, 1 - (ms - (ic.over_ms or ms)) / FRAME_MS / 25) or 1
ic.over_ms = ic.over and (ic.over_ms or ms) or nil
if fade > 0 then always.draw(self, (ms - ic.start) / FRAME_MS % always.len, fade) end
end
for i = #ic.active, 1, -1 do
local a = ic.active[i]
local f = (ms - a.start) / FRAME_MS
local fade = 1
if a.out then
local q = (ms - a.out) / FRAME_MS / a.def.fade
fade = a.gogo and (1 - q) * (1 - q) or 1 - q
end
if f >= a.pat.len or fade <= 0 then
table.remove(ic.active, i)
else
self.intercept_played = a.played
a.pat.draw(self, f, math.min(fade, 1))
end
end
self.intercept_played = nil
end
function EnsoBattleOverlay:boss_hit(ms)
local b = self.fight
b.hit = ms
if #b.hits >= BOSS_HIT.max then table.remove(b.hits, 1) end
b.hits[#b.hits + 1] = {
start = ms,
x = BOSS_HIT.x0 + math.random() * (BOSS_HIT.x1 - BOSS_HIT.x0),
y = BOSS_HIT.y0 + math.random() * (BOSS_HIT.y1 - BOSS_HIT.y0),
}
end
function EnsoBattleOverlay:draw_spark_hit(x, y, f)
if f >= 2 and f < 15 then
local u = (f - 2) / 13
self:spr("boss_hit_ring", x, y, { s = 0.3 + 1.02 * ease_out(u), fade = 1 - u })
end
if f < 4 then
self:spr("boss_hit_spark", x, y, { s = lerp(0.5, 1, f / 2) })
elseif f < 12 then
self:spr("boss_hit_spark", x, y, { frame = f < 8 and 1 or 2 })
elseif f < 16 then
self:spr("boss_hit_spark", x, y, { frame = 3, s = lerp(1, 1.2, (f - 12) / 4), fade = 0.852 })
end
end
function EnsoBattleOverlay:free_spot()
local best, best_gap = nil, -1
for _ = 1, 6 do
local x = FIELD.x0 + math.random() * (FIELD.x1 - FIELD.x0)
local y = FIELD.y0 + math.random() * (FIELD.y1 - FIELD.y0)
local gap = math.huge
for _, e in ipairs(self.enemies) do
gap = math.min(gap, (e.x - x) ^ 2 + ((e.y - y) * 2) ^ 2)
end
if gap > best_gap then best, best_gap = { x = x, y = y }, gap end
end
return best
end
function EnsoBattleOverlay:pop_enemy(ms, kind)
local spot = self:free_spot()
local enemies = self.enemy_stage.enemies
self.next_pop = math.max(ms, self.next_pop + POP_STAGGER_MS)
self.enemies[#self.enemies + 1] = {
key = enemies[kind + 1] or enemies[1], start = self.next_pop, x = spot.x, y = spot.y,
s = FIELD.far + (FIELD.near - FIELD.far) * (spot.y - FIELD.y0) / (FIELD.y1 - FIELD.y0),
}
end
function EnsoBattleOverlay:hit(ms, damage, good, critical, kat, big)
local target = self.enemies[1]
if target then target.hit = ms end
if self.fight then self:boss_hit(ms) end
local key = critical and "critical_digits" or good and "good_digits" or "ok_digits"
self.number = { value = damage, start = ms, key = key }
local face = critical and ("critical_" .. (kat and "kat" or "don")) or good and "good" or "ok"
local key = "hit_" .. face .. (big and "_big" or "")
self.lane_hits[#self.lane_hits + 1] = { key = key, start = ms, critical = critical }
if critical then play("se_critical") end
end
function EnsoBattleOverlay:roll_hit(ms, damage)
local target = self.enemies[1]
if target then target.hit = ms end
if self.fight then self:boss_hit(ms) end
if #self.rolls >= ROLL.max then table.remove(self.rolls, 1) end
self.rolls[#self.rolls + 1] = {
value = damage, start = ms, rise = math.random() < 0.5,
x = ROLL.x + (math.random() * 2 - 1) * ROLL.spread_x,
y = ROLL.y + (math.random() * 2 - 1) * ROLL.spread_y,
}
end
function EnsoBattleOverlay:kill(ms, skill)
local e = table.remove(self.enemies, 1)
local count = self.kill_count
if e and e.start <= ms then
e.start = ms
self.fallen[#self.fallen + 1] = e
end
if not skill then
count.target = count.target + 1
play("se_defeat")
return
end
local arrive = ms + SPARK.delay_ms + SPARK.fly_ms
local last = count.incoming[#count.incoming]
if last and last.at == arrive then
last.n = last.n + 1
else
count.incoming[#count.incoming + 1] = { at = arrive, n = 1 }
end
local from_x = e and e.x or SPARK.field_x0 + math.random() * (SPARK.field_x1 - SPARK.field_x0)
local from_y = e and e.y - 40 or 560
for _ = 1, SPARK.per_kill do
self.sparks[#self.sparks + 1] = {
start = ms + math.random() * SPARK.delay_ms,
x = from_x + (math.random() - 0.5) * 80, y = from_y + (math.random() - 0.5) * 60,
lift = 80 + math.random() * 160, small = math.random() < 0.5, spin = math.random() * 360,
}
end
end
function EnsoBattleOverlay:start_skill(ms, skill)
self.skill = { start = ms, skill = skill, move = SKILLS[skill] or {}, played = {} }
if self.fight then self.fight.knock, self.fight.knock_played = ms + BOSS_KNOCK.at, {} end
play(self.skill.move.cutin_sound or "se_cutin")
play("vo_skill", "voice")
end
function EnsoBattleOverlay:drop_chest(ms, orbs)
self.chests[#self.chests + 1] = { start = ms + SLASH_AT, x = 620 + math.random() * 120, y = 660,
kind = orbs and "orb_chest" or "chest" }
local cc = self.chest_count
cc.bumps[#cc.bumps + 1] = { at = ms + SLASH_AT + CHEST_COUNT.delay, orbs = orbs and true or false }
end
function EnsoBattleOverlay:song_end(ms, full_combo)
self.ended = ms
self.full_combo = full_combo
local b = self.fight
if b and b.hp <= 0 then
b.finish, b.finish_played = ms + FINISH.delay, {}
b.dead, b.dead_played = b.finish + FINISH.boss_dead, {}
else
self.end_banner, self.end_played = ms + FINISH.delay, {}
end
end
function EnsoBattleOverlay:set_status(power, fill, boss_hp)
self.power, self.fill = power, fill
if self.fight and boss_hp then self.fight.hp = boss_hp end
end
-- Marks the digits that differ between two numbers (counted from the right) to pop.
local function pop_changed(pops, old, new, ms)
old, new = tostring(old), tostring(new)
for i = 1, #new do
if old:sub(-i, -i) ~= new:sub(-i, -i) then pops[i] = ms end
end
end
function EnsoBattleOverlay:tick_counters(ms)
local count = self.kill_count
while count.incoming[1] and ms >= count.incoming[1].at do
local batch = table.remove(count.incoming, 1)
count.target = count.target + batch.n
local add = self.skill_add
if add and not add.leaving then
add.count, add.until_kills = add.count + batch.n, count.target
else
self.skill_add = { count = batch.n, start = ms, until_kills = count.target }
end
self.puff = { start = ms }
end
local gap = count.target - count.shown
if gap > 0 and ms >= count.next_tick then
local step = math.max(1, math.ceil(gap / KILL_TICK_CATCHUP))
pop_changed(count.pops, count.shown, count.shown + step, ms)
count.shown = count.shown + step
count.next_tick = ms + KILL_TICK_MS
if not self.skill_add then self.kill_add = { count = step, start = ms } end
end
local add = self.skill_add
if add and not add.leaving and count.shown >= add.until_kills then add.leaving = ms end
local power = self.power_count
if power.shown ~= self.power then
pop_changed(power.pops, power.shown, self.power, ms)
power.shown = self.power
end
end
function EnsoBattleOverlay:draw_enemy(e, ms)
local t = ms - e.start
if t < 0 then return end
local x, y, sx, sy, rot, fade = e.x, e.y, e.s, e.s, 0, 1
if t < POP_MS then
local p = t / POP_MS
if p < 0.33 then
local q = p / 0.33
x, y, fade = x + 56 * (1 - q), y - 27 * (1 - q), q
else
local q = (p - 0.33) / 0.67
y = y - 26 * math.sin(q * math.pi)
sy = e.s * (1 - 0.16 * math.max(0, math.sin((q - 0.5) * 2 * math.pi)))
end
else
sy = e.s * (1 - 0.045 * (1 - math.cos((t - POP_MS) / 650 * math.pi)))
end
if e.hit then
local h = ms - e.hit
if h < HIT_TILT_MS then
x, rot = x + 10, 30
elseif h < HIT_SHAKE_MS then
x = x + (math.random() - 0.5) * 2
end
end
if self.ended then
local q = clamp01((ms - self.ended) / ESCAPE_MS)
x, fade = x + 50 * q, fade * (1 - q)
end
self:spr(e.key, x, y, { ax = 0.5, ay = 1, sx = sx, sy = sy, rot = rot, fade = fade })
if e.hit and ms - e.hit < SPARK_MS then
local q = (ms - e.hit) / SPARK_MS
local _, h = self:size(e.key)
self:spr("defeat_flash", e.x, e.y - h * e.s / S / 2, { s = 0.4 + 0.3 * q, fade = 1 - q, add = true })
end
end
function EnsoBattleOverlay:draw_fallen(e, ms)
local q = (ms - e.start) / DEAD_MS
if q >= 1 then return true end
local _, h = self:size(e.key)
local mid = e.y - h * e.s / S / 2
self:spr(e.key, e.x + 570 * q, mid - 190 * ease_out(q), { s = e.s, rot = 30 + 105 * q, fade = 1 - q })
self:spr("defeat_flash", e.x, mid, { s = 0.6 + 0.8 * q, fade = 1 - q, add = true })
return false
end
function EnsoBattleOverlay:draw_amitie(ms)
local frame, bob = 0, math.sin(ms / 400) * 2
if self.skill then
local t = ms - self.skill.start
frame = t < SLASH_AT and 2 or 3
end
tex.draw_texture(self:id("amitie"), { y = bob * S, frame = frame })
end
function EnsoBattleOverlay:draw_chest(c, ms)
local t = ms - c.start
if t < 0 then return false end
if t >= CHEST_MS then return true end
local drop = clamp01(t / 300)
local y = c.y - 220 * (1 - ease_out(drop))
local fade = clamp01((CHEST_MS - t) / 300)
if t < 600 then
self:spr("chest_beam", c.x, c.y, { ay = 1, fade = 1 - t / 600, add = true })
end
self:spr("chest_halo", c.x, c.y - 20, { fade = fade * 0.8, add = true })
self:spr(c.kind, c.x, y, { ay = 1, fade = fade })
if drop >= 1 then
local q = clamp01((t - 300) / 400)
self:spr(c.kind .. "_ring", c.x, c.y - 72, { s = 0.5 + q, fade = (1 - q) * fade, add = true })
end
return false
end
-- plays `name` once, the first frame `ready` holds
local function cue(played, ready, name, preset)
if ready and not played[name] then
played[name] = true
play(name, preset)
end
end
-- offset, squash and visibility while the special move throws the boss away and it falls back
function EnsoBattleOverlay:boss_knock(b, ms)
local k = ms - b.knock
if k < 0 then return nil end
local K = BOSS_KNOCK
cue(b.knock_played, true, "se_boss_knock")
cue(b.knock_played, k >= K.back, "se_boss_fall")
cue(b.knock_played, k >= K.land, "se_boss_land")
if k < K.out_ms then
local q = k / K.out_ms
return { dx = lerp(44, 919, q), dy = lerp(-23, -487, q) }
elseif k < K.back then
return { hidden = true }
elseif k < K.land then
local q = (k - K.back) / (K.land - K.back)
return { dx = lerp(117, 0, q), dy = lerp(-1007, 0, q), back = true }
elseif k < K.settle then
return { sy = lerp(0.99, 0.918, (k - K.land) / (K.settle - K.land)), back = true }
elseif k < K.done then
return { sy = lerp(0.926, 1, (k - K.settle) / (K.done - K.settle)), back = true }
end
b.knock = nil
return nil
end
function EnsoBattleOverlay:draw_boss_dead(b, ms)
local k, B = ms - b.dead, b.def
local D = B.dead
cue(b.dead_played, k >= D.hold, "se_boss_shake")
cue(b.dead_played, k >= D.burst, "se_boss_vanish")
if k < D.burst then
local dx = k < D.hold and 0 or math.floor((k - D.hold) / 50) % 2 == 0 and 10 or -10
local dying = D.hold > 0 and clamp01(k / D.hold) or 1
B.draw(self, { t = math.huge, idle = 0, dx = dx, dy = 0, sy = 1, dying = dying })
return
end
-- a boss without a single burst picture draws its dying pose scaled
local function burst(s, fade, add)
if D.key then return self:spr(D.key, D.x, D.y, { s = s, fade = fade, add = add }) end
B.draw(self, { t = math.huge, idle = 0, dx = 0, dy = 0, sy = 1, dying = 1, s = s, fade = fade, add = add })
end
local q = (k - D.burst) / D.fade_ms
if q < 1 then burst(lerp(D.s or 1, D.grow or D.s or 1, q), 1 - q) end
if D.glow then
local g = ease_out(clamp01((k - D.burst) / D.glow_ms))
burst(lerp(D.s or 1, D.glow, g), 1 - g, true)
end
end
function EnsoBattleOverlay:draw_boss(ms)
local b = self.fight
if b.dead and ms >= b.dead then return self:draw_boss_dead(b, ms) end
local t = ms - b.start
local p = { t = t, idle = t >= b.def.pop_ms and t - b.def.pop_ms or nil, dx = 0, dy = 0, sy = 1 }
if b.hit and ms - b.hit < BOSS_DMG_MS then p.hurt = (ms - b.hit) / BOSS_DMG_MS end
local knock = b.knock and self:boss_knock(b, ms)
if knock then
if knock.hidden then return end
p.knock, p.dx, p.dy, p.sy = knock, knock.dx or 0, knock.dy or 0, knock.sy or 1
end
b.def.draw(self, p)
for i = #b.hits, 1, -1 do
local h = b.hits[i]
local k = ms - h.start
if k >= BOSS_HIT.ms then
table.remove(b.hits, i)
elseif not knock and b.def.spark_hit then
self:draw_spark_hit(h.x, h.y, k / FRAME_MS)
elseif not knock then
local hit_fade = k < 83 and lerp(0.89, 1, k / 83) or lerp(0.91, 0, (k - 100) / 167)
self:spr("boss_hit", h.x, h.y, { fade = hit_fade })
end
end
end
function EnsoBattleOverlay:draw_win_banner(t)
local F = FINISH
local band_sx, band_x, band_sy, band_fade = 15.35, 0, 1, 1
if t < 100 then
local q = t / 100
band_sx, band_x, band_sy, band_fade = lerp(1.8, 15.35, q), lerp(-881, 0, q), lerp(1, 1.155, q), q
elseif t < 333 then
band_sy = lerp(1.155, 1, (t - 100) / 233)
elseif t < 1167 then
band_sy = lerp(1, 0.904, (t - 333) / 834)
else
band_sy = 1.013 - 0.111 * math.cos((t - 1167) / 1833 * 2 * math.pi)
end
for _ = 1, 2 do
self:spr("boss_win_band", F.x + band_x, F.y, { sx = band_sx, sy = band_sy, fade = band_fade })
end
if t < 650 then
local sx, sy, x, fade = 1, 0.167, 0, 1
if t < 167 then
local q = t / 167
sx, sy, x, fade = lerp(0.126, 1, q), lerp(2.53, 0.167, q), lerp(-877, 0, q), q
elseif t >= 333 then
local q = clamp01((t - 333) / 167)
sy, fade = lerp(0.417, 3.33, q * q), lerp(0.89, 0, (t - 500) / 150)
if t < 500 then fade = 1 end
end
self:spr("boss_win_line", F.x + x, F.y + 2, { sx = sx, sy = sy, fade = fade })
end
if self.full_combo then
self:draw_fc_text(t)
return
end
if t >= 500 then
local s, fade = 2, 0.77
if t < 667 then
s, fade = lerp(1.68, 2, (t - 500) / 167), (t - 500) / 167
elseif t < 1167 then
fade = lerp(0.965, 0.77, (t - 667) / 500)
else
s = 1.833 + 0.167 * math.cos((t - 1167) / 1833 * 2 * math.pi)
end
self:spr("boss_win_glow", F.x, F.y, { s = s, fade = fade, add = true })
end
local x, fade = 0, 1
if t < 283 then
x, fade = lerp(-776, 256, t / 283), lerp(0.06, 1, t / 283)
elseif t < 333 then
x = lerp(170, 0, (t - 283) / 50)
end
local pop = (t >= 433 and t < 567) and math.sin((t - 433) / 133 * math.pi) or 0
self:spr("boss_win_banner", F.x + x, F.y + 5, { s = 1 + 0.167 * pop, fade = fade })
if pop > 0 then
self:spr("boss_win_banner", F.x, F.y + 5, { s = 1 + 0.167 * pop, fade = 1 - (t - 433) / 133, add = true })
end
end
function EnsoBattleOverlay:draw_fc_text(t)
local F = FINISH
local f = t / FRAME_MS
local x, y = F.x + FC.dx, F.y + FC.dy
if f >= FC.band_from then
local b = f - FC.band_from
local sx, sy, fade = 15.32, 1, 0.8
if b < 4 then
local q = b / 4
sx, sy, fade = lerp(3.23, 15.32, q), lerp(1, 1.88, q), lerp(0, 0.8, q)
else
local c = math.cos((b - 4) / FC.band_period * 2 * math.pi)
sy, fade = 1.555 + 0.325 * c, 0.895 - 0.095 * c
end
self:spr("fc_band_flash", F.x, F.y, { sx = sx, sy = sy, fade = fade, add = true })
end
local p = f < FC.loop_from and f or FC.loop_from + (f - FC.loop_from) % FC.loop_len
local pulse, flash = 1, window(p, 82, 88, 88, 95)
if p >= 82 and p < 89 then
pulse = lerp(1.014, 1.097, (p - 82) / 7)
elseif p >= 89 and p < 96 then
pulse = lerp(1.083, 1, (p - 89) / 7)
end
if f < 44 then
local rise, white, glow = -15, 1, 1
if f < 15 then
local q = f / 15
rise, white, glow = -15 * ease_out(q), q, q
elseif f >= 25 and f < 38 then
local q = (f - 25) / 12
rise, white, glow = -15 * (1 - q * q), 1 - q, lerp(1, 0.5, q)
elseif f >= 38 then
rise, white, glow = 0, 0, lerp(0.42, 0, (f - 38) / 6)
end
self:spr("fc_text_flash", x, y + rise, { fade = clamp01(white) })
self:spr("fc_text_glow", x, y + rise, { fade = clamp01(glow), add = true })
elseif flash > 0 then
self:spr("fc_text_flash", x, y, { s = pulse, fade = 0.32 * flash })
self:spr("fc_text_glow", x, y, { s = pulse, fade = flash, add = true })
end
if f < 24 then return end
local dy, sy = 0, 1
if f < 38 then
local q = (f - 24) / 13
dy = -15 * (1 - q * q)
elseif f < 41 then
sy, dy = lerp(0.939, 0.818, (f - 38) / 2), lerp(2.35, 7, (f - 38) / 2)
elseif f < 44 then
sy, dy = lerp(0.879, 1, (f - 41) / 2), lerp(4.7, 0, (f - 41) / 2)
end
local flicker = 0.8 + 0.2 * (1 - math.abs(2 * (f % 20) / 20 - 1))
self:spr("fc_text_glow", x, y + dy, { sx = pulse, sy = sy * pulse, fade = flicker, add = true })
self:spr("fc_text", x, y + dy, { sx = pulse, sy = sy * pulse })
end
function EnsoBattleOverlay:draw_end_banner(ms)
local t = ms - self.end_banner
if t < 0 then return end
local F = FINISH
local fc = self.full_combo
cue(self.end_played, true, fc and "se_fc_start" or "se_battle_end")
cue(self.end_played, t >= 333, fc and "se_fc_banner" or "se_battle_end_banner")
if fc then cue(self.end_played, t >= 333, "vo_fullcombo", "voice") end
local copies, band_sx, band_x, band_sy, band_fade = 2, 15.35, 0, 1, 1
if t < 167 then
band_sx, band_x, band_fade = lerp(0.108, 15.35, t / 167), lerp(-473, 0, t / 167), t / 167
elseif t < 1317 then
band_sy, band_fade = lerp(1, 1.126, (t - 167) / 1150), lerp(1, 0, (t - 167) / 1150)
elseif t < 1483 then
copies, band_sy, band_fade = 1, 1.128, lerp(0, 0.9, (t - 1317) / 166)
else
band_fade = (1 + math.cos((t - 1483) / 2017 * 2 * math.pi)) / 2
band_sy = 0.902 + 0.226 * band_fade
end
for _ = 1, copies do
self:spr("boss_win_band", F.x + band_x, F.y, { sx = band_sx, sy = band_sy, fade = band_fade })
end
if t >= 83 and t < 733 then
local sx, sy, x, fade = 1, 0.017, 0, window(t, 83, 167, 517, 733)
if t < 167 then
local q = (t - 83) / 84
sx, sy, x = lerp(0.187, 1, q), lerp(2.533, 0.167, q), lerp(-386, 0, q)
elseif t < 267 then
sy = lerp(0.137, 0.017, (t - 183) / 67)
else
local q = clamp01((t - 267) / 250)
sy = lerp(0.047, 3.332, q * q)
end
self:spr("boss_win_line", F.x + x, F.y + 2, { sx = sx, sy = sy, fade = fade })
end
if t < 333 then return end
if fc then
self:draw_fc_text(t - 333)
return
end
local sy = 1
if t < 517 then
sy = lerp(0.08, 1.8, (t - 333) / 184)
elseif t < 683 then
sy = lerp(1.8, 0.4, (t - 517) / 166)
elseif t < 733 then
sy = lerp(0.6, 0.8, (t - 700) / 17)
end
self:spr("battle_end_banner", F.x, F.y, { sy = sy, fade = clamp01((t - 333) / 184) })
local glow = window(t, 333, 517, 517, 733)
if glow > 0 then self:spr("battle_end_banner", F.x, F.y, { sy = sy, fade = glow, add = true }) end
end
function EnsoBattleOverlay:draw_finish(b, ms)
local t = ms - b.finish
if t < 0 then return end
local F = FINISH
local fc = self.full_combo
if fc then cue(b.finish_played, true, "se_boss_fc_start") end
cue(b.finish_played, t >= 300, "se_boss_sword")
cue(b.finish_played, t >= F.shatter, "se_boss_break")
if fc then
cue(b.finish_played, t >= F.banner, "se_boss_fc_banner")
cue(b.finish_played, t >= F.banner, "vo_fullcombo", "voice")
else
cue(b.finish_played, t >= F.banner + 433, "vo_boss_win", "voice")
end
if t >= F.banner then self:draw_win_banner(t - F.banner) end
if t < 1183 then
local dx = t < 583 and lerp(-5, 5, t / 583) or (math.floor((t - 583) / 33) % 2 == 0 and -4 or 5)
self:spr("boss_seal", F.seal_x + dx, F.seal_y, { fade = clamp01(t / 583) })
elseif t >= F.shatter and t < F.shatter + F.shard_ms then
local q = (t - F.shatter) / F.shard_ms
for i, s in ipairs(SHARDS) do
self:spr("boss_seal_shards", F.seal_x + lerp(s[1], s[3], q), F.seal_y + lerp(s[2], s[4], q), {
frame = i - 1, s = lerp(1, s[5], q), fade = 1 - q,
})
end
end
local slash_x, slash_y = F.seal_x + F.slash_dx, F.seal_y + F.slash_dy
if t >= 300 and t < 517 then
local fade = t < 333 and (t - 300) / 33 or t < 450 and 0.95 or lerp(0.89, 0, (t - 450) / 67)
self:spr("boss_seal_slash", slash_x + 10 * (t - 300) / 217, slash_y, { s = 0.732, fade = fade })
elseif t >= 500 and t < 717 then
local fade = t < 517 and (t - 500) / 17 or t < 667 and 0.93 or lerp(0.67, 0, (t - 667) / 50)
self:spr("boss_seal_slash", slash_x, slash_y, { s = 0.74, rot = 45, fade = fade })
end
end
function EnsoBattleOverlay:draw_bar(key, frac, dy)
local w, h = self:size(key)
tex.draw_texture(self:id(key), {
src = { x = w / S * (1 - frac), y = 0, width = w / S * frac, height = h / S },
x = w * (1 - frac), y = dy * S, x2 = w * frac - w,
})
end
function EnsoBattleOverlay:draw_boss_gauge(ms)
local b, G = self.fight, BOSS_GAUGE
if b.hp < b.shown then b.bump = ms end
b.shown = b.hp
local dy = b.bump and ms - b.bump < G.bump_ms and -2 or 0
local r, from = b.hp / b.max, b.from / b.max
tex.draw_texture(self:id("boss_gauge_back"), { y = dy * S })
self:draw_bar("boss_gauge_lost", from, dy)
if r > 0 then
self:draw_bar(r > 2 / 3 and "boss_gauge_high" or r > G.warn and "boss_gauge_mid" or "boss_gauge_low", r, dy)
end
tex.draw_texture(self:id("boss_gauge_frame"), { y = dy * S })
if r < G.critical then
tex.draw_texture(self:id("boss_gauge_warn"), { y = dy * S })
local pulse = 0.5 + 0.5 * math.cos(ms / G.critical_ms * 2 * math.pi)
tex.draw_texture(self:id("boss_gauge_warn_glow"), { y = dy * S, fade = pulse })
elseif r < G.warn then
local pulse = 0.5 + 0.5 * math.cos(ms / G.warn_ms * 2 * math.pi)
tex.draw_texture(self:id("boss_gauge_warn"), { y = dy * S, fade = pulse })
end
if from < 1 then
tex.draw_texture(self:id("boss_gauge_needle"), { x = G.w * (1 - from) * S, y = dy * S })
end
end
function EnsoBattleOverlay:draw_back(ms)
local zone = self.fight and self.stage or self.enemy_stage
tex.draw_texture(self:id(zone.bg), {})
if self.fight and zone.scenery then
local sc = zone.scenery
self:spr(sc[1], sc[2], sc[3], { ax = 0, ay = 0 })
end
self:draw_amitie(ms)
if self.fight then self:draw_boss(ms) end
for i = #self.fallen, 1, -1 do
if self:draw_fallen(self.fallen[i], ms) then table.remove(self.fallen, i) end
end
local order = {}
for i, e in ipairs(self.enemies) do order[i] = e end
table.sort(order, function(a, b) return a.y < b.y end)
for _, e in ipairs(order) do self:draw_enemy(e, ms) end
for i = #self.chests, 1, -1 do
if self:draw_chest(self.chests[i], ms) then table.remove(self.chests, i) end
end
end
function EnsoBattleOverlay:draw_number(key, value, right, cy, gap, o)
o = o or {}
local s = o.s or 1
local digits = tostring(math.floor(value))
local step = (self:size(key) / S + gap) * s
local x = right - step * #digits + step / 2
for i = 1, #digits do
local pop = o.pops and o.pops[#digits - i + 1]
local ds = s
if pop then ds = s * (1 + 0.4 * (1 - clamp01((o.ms - pop) / DIGIT_POP_MS))) end
self:spr(key, x, cy, { frame = tonumber(digits:sub(i, i)), s = ds, fade = o.fade })
x = x + step
end
end
function EnsoBattleOverlay:draw_damage(ms)
local n = self.number
if not n then return end
local t = ms - n.start
if t >= NUMBER.life_ms then
self.number = nil
return
end
local q = clamp01(t / NUMBER.drop_ms)
local y = NUMBER.y + NUMBER.drop_from + (NUMBER.drop_to - NUMBER.drop_from) * q
local fade = 0.5 + 0.5 * q
if t > NUMBER.life_ms - 67 then fade = (NUMBER.life_ms - t) / 67 end
local half = (self:size(n.key) / S - 4) * #tostring(n.value) / 2
self:draw_number(n.key, n.value, NUMBER.x + half, y, -4, { fade = fade })
end
function EnsoBattleOverlay:draw_rolls(ms)
for i = #self.rolls, 1, -1 do
local r = self.rolls[i]
local t = ms - r.start
if t >= ROLL.bg_ms then
table.remove(self.rolls, i)
else
local bg = t < 83 and lerp(0.6, 0.8, t / 83) or t < 417 and 0.8 or lerp(0.71, 0, (t - 417) / 133)
self:spr("roll_burst", r.x, r.y, { s = ROLL.s, fade = bg })
if t < ROLL.number_ms then
local fade = t < 100 and lerp(0.6, 1, t / 100) or t < 350 and 1 or lerp(0.89, 0, (t - 350) / 133)
local y = r.y
if r.rise then
y = y - (t < 100 and lerp(0, 20.6, t / 100) or t < 350 and 20.6 or lerp(22, 35.6, (t - 350) / 133))
end
local half = (self:size("ok_digits") / S - 4) * ROLL.s * #tostring(r.value) / 2
self:draw_number("ok_digits", r.value, r.x + half, y, -4, { s = ROLL.s, fade = fade })
end
end
end
end
function EnsoBattleOverlay:draw_sparks(ms)
for i = #self.sparks, 1, -1 do
local p = self.sparks[i]
local t = ms - p.start
if t >= SPARK.fly_ms then
table.remove(self.sparks, i)
elseif t >= 0 then
local q = t / SPARK.fly_ms
local e = q * q
local x = p.x + (SPARK.x - p.x) * e
local y = p.y + (SPARK.y - p.y) * e - p.lift * math.sin(q * math.pi)
local twinkle = 0.75 + 0.25 * math.sin((ms + p.spin) / 40)
self:spr(p.small and "kill_sparkle_small" or "kill_sparkle", x, y, {
s = lerp(1.4, 0.8, q) * twinkle, rot = p.spin + q * 90, fade = math.min(1, t / 80), add = true,
})
end
end
local puff = self.puff
if puff then
local q = (ms - puff.start) / 500
if q >= 1 then
self.puff = nil
else
self:spr("kill_puff", SPARK.x + 40, SPARK.y, { s = lerp(0.6, 1.2, q), fade = (1 - q) * 0.9, add = true })
end
end
end
-- "+N" with its sign: frame 10 of the digit sheet
function EnsoBattleOverlay:draw_add(key, count, x, y, fade, s)
local w = (self:size(key) / S + KILL_ADD.gap) * s
self:spr(key, x + w / 2, y, { frame = 10, fade = fade, s = s })
self:draw_number(key, count, x + w * (1 + #tostring(count)), y, KILL_ADD.gap, { fade = fade, s = s })
end
function EnsoBattleOverlay:draw_best_banner(ms)
local f = (ms - self.best_banner) / FRAME_MS
if f >= BEST_BANNER.len then return end
local x, y = BEST_BANNER.x, BEST_BANNER.y
if f < BEST_BANNER.slide then
local q = f / BEST_BANNER.slide
x, y = lerp(BEST_BANNER.x_off, x, q), lerp(y - 6, y, q)
elseif f >= BEST_BANNER.out then
x = lerp(x - 13, BEST_BANNER.x_off - 15, (f - BEST_BANNER.out) / 12)
end
local glow = f >= BEST_BANNER.slide and f < BEST_BANNER.out
local loop = f - BEST_BANNER.slide
local pulse = 0.5 + 0.5 * math.cos(2 * math.pi * loop / 60)
if glow then self:spr("kill_puff", BEST_BANNER.x + 2, y - 0.5, { fade = pulse }) end
local breathe = glow and 1 - 0.055 * (1 - math.cos(2 * math.pi * (loop - 1) / 60)) / 2 or 1
self:spr("best_banner_cloud", x, y, { s = breathe })
if glow then
local drift = loop >= 61 and 5.8 * (1 - math.abs(loop - 91) / 30) or 0
self:spr("best_banner_glow", BEST_BANNER.x, y - 1 - drift, { fade = pulse })
end
self:spr("best_banner_text", x, y)
if glow then
local p = loop % 60
local q = p % 30
local fade = q < 15 and q / 15 or 0.93 * (1 - (q - 15) / 14)
self:spr(p < 30 and "kill_sparkle_small" or "kill_sparkle", BEST_BANNER.x + 2, y, { fade = fade })
end
end
function EnsoBattleOverlay:draw_kill_add(ms)
local a = self.kill_add
if not a then return end
local t = ms - a.start
if t >= 600 then
self.kill_add = nil
return
end
local dx, dy, fade = 0, 0, 1
if t < 83 then
local q = t / 83
dx, dy, fade = 10 * (1 - q), 10 * (1 - q), 0.5 + 0.5 * q
elseif t >= 500 then
local q = (t - 500) / 100
dx, dy, fade = -10 * q, 10 * q, 1 - q
end
self:draw_add("kill_add_digits", a.count, KILL_ADD.x + dx, KILL_ADD.cy + dy, fade, 1)
end
function EnsoBattleOverlay:draw_skill_add(ms)
local a = self.skill_add
if not a then return end
local t = ms - a.start
local dx, dy, fade, s = 0, 0, 1, 1
if t < 117 then
dx = 30 * (1 - t / 117)
elseif t < 467 then
dy = -math.sin((t - 117) / 350 * math.pi) * 1.5
end
if a.leaving then
local q = (ms - a.leaving) / 150
if q >= 1 then
self.skill_add = nil
return
end
dx, s, fade = -46 * q, 0.94 - 0.52 * q, 1 - q
end
self:draw_add("kill_skill_digits", a.count, KILL_ADD.x + dx, KILL_ADD.cy + dy, fade, s)
end
function EnsoBattleOverlay:draw_lane_hit(h, ms)
local q = (ms - h.start) / LANE_HIT_MS
if q >= 1 then return true end
self:spr(h.key, self.judge.x, self.judge.y, { s = 0.8 + 0.4 * q, fade = 1 - q, add = true })
if h.critical then
self:spr("hit_critical_rays", self.judge.x, self.judge.y, {
s = 0.9 + 0.5 * q, rot = 20 * q, fade = 1 - q, add = true,
})
end
return false
end
function EnsoBattleOverlay:draw_gauge(ms)
tex.draw_texture(self:id("skill_gauge_bubble"), {})
local w = GAUGE_FILL_WIDTH * S
if self.fill > 0 then
tex.draw_texture(self:id("skill_gauge_fill"), { x2 = w * (self.fill - 1) })
end
tex.draw_texture(self:id("skill_gauge_frame"), {})
if self.fill >= 1 then
local pulse = 0.5 + 0.5 * math.sin(ms / 120)
tex.draw_texture(self:id("skill_gauge_full"), { fade = pulse, blend = "additive" })
tex.draw_texture(self:id("skill_gauge_glow"), { frame = math.floor(ms / 50) % 5, blend = "additive" })
end
end
local SPIRIT_SABER = { start = "slash_start", main = "slash", glow = "slash_glow", delay = 0, dx = 0 }
function EnsoBattleOverlay:draw_slash(t, keys)
local f = (t - SLASH_AT) / FRAME_MS - keys.delay
if f < 0 then return end
local drift = 36 * clamp01((f - 4) / 56)
local master = clamp01(f / 3) * lerp(1, 0.8, (f - 4) / 56) * (1 - clamp01((f - 57) / 17))
local x, cx = SLASH.x + drift, SLASH.cx + drift + keys.dx
if f < 6 then
self:spr("slash_blur", cx - 179, SLASH.cy - 72, { s = 1.746, rot = -4, fade = 0.91 * (1 - f / 6), add = true })
end
if f < 8 then
self:spr(keys.start, x, SLASH.y, { ax = 0, ay = 0, s = SLASH.s, fade = master * lerp(0, 0.75, f / 3) })
end
if f >= 4 and f < 26 then
local q = (f - 4) / 22
self:spr(keys.glow, cx, SLASH.cy, {
s = SLASH.s * lerp(1, 1.134, q), fade = master * 0.5 * (1 - q), add = true,
})
end
if f >= 8 and f < 30 then
local q = (f - 8) / 22
self:spr(keys.glow, cx, SLASH.cy, {
s = SLASH.s * lerp(1, 1.223, q), fade = master * 0.2 * (1 - q), add = true,
})
end
if f >= 4 and f < 68 then
local fade = f < 50 and 1 or lerp(0.97, 0.34, (f - 50) / 18)
self:spr(keys.main, x, SLASH.y, { ax = 0, ay = 0, s = SLASH.s, fade = master * fade })
end
end
function EnsoBattleOverlay:draw_cutin(ms)
if not self.skill then return end
local t = ms - self.skill.start
if t >= CUTIN_MS then
self.skill = nil
return
end
local sk = self.skill
local move = sk.move
local f = t / FRAME_MS + 4
if sk.skill == 1 then
if t >= CUTIN_IN and not sk.swung then
sk.swung = true
play("se_slash")
end
if t >= SLASH_AT and not sk.slashed then
sk.slashed = true
play("se_slash_hit")
end
end
for i, cue in ipairs(move.sounds or {}) do
if f >= cue[1] and not sk.played[i] then
sk.played[i] = true
play(cue[2])
end
end
if t < CUTIN_IN + 100 then
local veil = math.min(t / 300, 1) * (1 - clamp01((t - CUTIN_IN) / 100))
tex.draw_rect(0, 360 * S, 1280 * S, 360 * S, 255, 255, 255, math.floor(90 * veil))
self:spr("cutin_band", 0, 350, { ax = 0, ay = 0, sx = lerp(0.05, 0.2, t / CUTIN_IN), fade = veil, add = true })
end
local out = clamp01((t - CUTIN_OUT) / 300)
local fade = 1 - out
if move.back then move.back(self, f) end
if t < CUTIN_IN then return end
local band = sk.skill - 1
local band_x = lerp(-1021, -279, (t - CUTIN_IN) / 100)
local band_sx = lerp(0.859, 1, out)
local swap = 0.5 + 0.5 * math.sin(t / 1200 * 2 * math.pi)
self:spr("cutin_band", band_x, 350, { ax = 0, ay = 0, sx = band_sx, frame = band, fade = fade })
self:spr("cutin_band", band_x, 350, { ax = 0, ay = 0, sx = band_sx, frame = band, fade = fade * (1 - swap) })
self:spr("cutin_band_alt", band_x, 350, { ax = 0, ay = 0, sx = band_sx, frame = band, fade = fade * swap })
local lines_x = t < CUTIN_OUT and lerp(-651, -241, (t - 517) / 67) or lerp(-241, -172, out)
local lines_fade = window(t, 517, 583, CUTIN_OUT, CUTIN_MS) * 0.5
local alt = math.floor(t / (3 * FRAME_MS)) % 2 == 0
self:spr(alt and "cutin_lines" or "cutin_lines_alt", lines_x, 350, { ax = 0, ay = 0, fade = lines_fade })
local p = t - CUTIN_IN
local amitie_x = -289 - 15 * (1 - clamp01(p / 1617)) - 797 * math.exp(-p / 68)
if move.mid then move.mid(self, f) end
if move.portrait then
move.portrait(self, f, amitie_x, fade)
else
self:spr("cutin_amitie", amitie_x, 350, { ax = 0, ay = 0, fade = fade })
end
if sk.skill == 1 or move.slash then self:draw_slash(t, move.slash or SPIRIT_SABER) end
if move.front then move.front(self, f) end
end
-- the counter's glows: in, dip, back up, out over 100 frames
local function count_glow(t)
local g = t < 11 and t / 10 or t < 32 and lerp(0.98, 0.65, (t - 11) / 20)
or t < 52 and lerp(0.67, 1, (t - 32) / 19) or lerp(0.98, 0, (t - 52) / 47)
return clamp01(g)
end
-- one badge: the count, popping 1.5 -> 1 with a fading glow and a widening ring when it goes up
function EnsoBattleOverlay:draw_chest_badge(key, n, y, bump, ms)
local C = CHEST_COUNT
local t = bump and (ms - bump) / FRAME_MS
local s = 1
if t and t < 9 then s = t < 5 and lerp(1, 1.5, t / 4) or lerp(1.375, 1, (t - 5) / 3) end
if t and t < 110 then
self:spr("chest_count_glow", C.badge_x, y, { fade = count_glow(t) })
end
self:spr(key, C.badge_x, y, { s = s })
self:spr("chest_count_digits", C.badge_x, y, { s = s, frame = math.min(n, 6) })
if t and t < 9 then self:spr("chest_count_ring", C.badge_x, y, { s = lerp(1, 2.083, t / 8), fade = 1 - t / 8 }) end
end
function EnsoBattleOverlay:draw_chest_count(ms)
local C, cc = CHEST_COUNT, self.chest_count
local n, orbs, last, last_orb = 0, 0, nil, nil
for _, b in ipairs(cc.bumps) do
if ms >= b.at then
if b.orbs then orbs, last_orb = orbs + 1, b.at else n, last = n + 1, b.at end
end
end
local latest = math.max(last or -1e9, last_orb or -1e9)
local t = (ms - latest) / FRAME_MS
local s = 1
if t >= 0 and t < 11 then s = t < 4 and lerp(1.034, 1.136, t / 3) or lerp(1.114, 1, (t - 4) / 6) end
if t >= 0 and t < 100 then
self:spr("chest_count_icon_glow", C.x, C.y, { fade = count_glow(t) })
end
self:spr("chest_count_icon", C.x, C.y, { s = s })
if t >= 0 and t < 14 then
local k = t < 4 and lerp(0.364, 1, t / 3) or lerp(1.036, 1.364, (t - 4) / 9)
local fade = t < 4 and t / 3 or 0.9 * (1 - (t - 4) / 9)
self:spr("chest_count_sparkle", C.badge_x, C.badge_y - 2, { s = k, fade = fade })
end
self:draw_chest_badge("chest_count_badge", n, C.badge_y, last, ms)
if cc.orbs then self:draw_chest_badge("chest_count_orb_badge", orbs, C.orb_y, last_orb, ms) end
end
function EnsoBattleOverlay:draw_front(ms)
self:draw_intercept(ms)
self:draw_chest_count(ms)
for i = #self.lane_hits, 1, -1 do
if self:draw_lane_hit(self.lane_hits[i], ms) then table.remove(self.lane_hits, i) end
end
self:draw_damage(ms)
self:draw_rolls(ms)
self:tick_counters(ms)
if self.fight then
self:draw_boss_gauge(ms)
else
tex.draw_texture(self:id("kill_panel"), {})
local kills = self.kill_count
self:draw_number("kill_digits", kills.shown, KILL_DIGITS.right, KILL_DIGITS.cy, KILL_DIGITS.gap,
{ pops = kills.pops, ms = ms })
if self.skill_add then self:draw_skill_add(ms) else self:draw_kill_add(ms) end
self:draw_sparks(ms)
end
if self.fight and self.fight.finish then self:draw_finish(self.fight, ms) end
if self.end_banner then self:draw_end_banner(ms) end
tex.draw_texture(self:id("power_panel"), {})
self:draw_number("power_digits", self.power, POWER_DIGITS.right, POWER_DIGITS.cy, POWER_DIGITS.gap,
{ pops = self.power_count.pops, ms = ms })
if self.best_power and not self.best_banner and self.power > self.best_power then self.best_banner = ms end
if self.best_banner then self:draw_best_banner(ms) end
self:draw_gauge(ms)
end