Files
2026-10-11 15:13:35 -04:00

168 lines
5.9 KiB
Lua

AttractChara = {}
AttractChara.__index = AttractChara
local FRAME = 1000 / 60
local START_MS = 3000
local GAP_MS = 500
local function ease_out(t) return t * (2 - t) end
local function ease_in(t) return t * t end
local function lerp(a, b, t) return a + (b - a) * t end
local function span(t, t0, t1) return math.max(0, math.min(1, (t - t0) / (t1 - t0))) end
-- Shiba: rises from below, overshoots, settles, then sinks away
local SHIBA_MS = 3100
local SHIBA_REST_Y = 202.5
local SHIBA_RISE = { from = 632.5, top = 157.5, ms = 333, settle_ms = 167 }
local SHIBA_SINK = { start = 2533, ms = 333, to = 590.5 }
local SHIBA_WAG_START = 500
-- breath puffs: { index in texture.json, ms they pop at, fades out linearly }
local PUFFS = {
{ 1, 500 }, { 2, 750, true }, { 3, 1000 },
{ 1, 1700 }, { 2, 1933, true }, { 3, 2167 },
}
local PUFF_POP_MS = 100
local PUFF_FADE_MS = 833
-- Fox-mask bear: slides in from the left, waves, slides back out
local FOX_MS = 3583
local FOX_X = { from = 11.5, rest = 246.5, to = 16.5, in_ms = 500, out_at = 3167, out_ms = 417 }
-- Chicks: { texture, flap ms per frame, first flap, path of { frame, x, y } }
local BIRDS_MS = 3917
local BIRDS = {
{ "bird_yellow", 5, 21, { { 0, 742, -16 }, { 20, 742, 128 }, { 21, 742, 133 }, { 139, 742, 598 } } },
{ "bird_pink", 5, 40, { { 0, 153, 114 }, { 15, 249, 66 }, { 16, 257, 66 }, { 28, 332, 111 }, { 39, 371, 184 }, { 40, 374, 185 }, { 159, 366, 594 } } },
{ "bird_green", 8, 39, { { 39, 501, -8 }, { 198, 501, 593 } } },
{ "bird_blue", 5, 111, { { 49, 994, 94 }, { 70, 877, 61 }, { 71, 871, 61 }, { 110, 678, 164 }, { 111, 676, 170 }, { 234, 594, 597 } } },
}
local SEQUENCES = { { "shiba", SHIBA_MS }, { "fox", FOX_MS }, { "birds", BIRDS_MS } }
local LAP_MS = 0
for _, s in ipairs(SEQUENCES) do LAP_MS = LAP_MS + s[2] + GAP_MS end
function AttractChara.new()
local self = setmetatable({}, AttractChara)
self.start_ms = tex.get_current_ms()
self.active = nil
self.t = 0
self.tail_wag = tex.get_animation(46, "title")
self.fox_poses = tex.get_animation(47, "title")
self.t_shiba = tex.get_texture("title/camera/shiba")
self.t_tail = tex.get_texture("title/camera/shiba_tail")
self.t_puff = tex.get_texture("title/camera/shiba_puff")
self.t_fox = tex.get_texture("title/camera/fox")
self.t_birds = {}
for _, b in ipairs(BIRDS) do
self.t_birds[b[1]] = tex.get_texture("title/camera/" .. b[1])
end
return self
end
function AttractChara:update(current_ms)
local now = current_ms - self.start_ms - START_MS
local active
if now >= 0 then
local at = now % LAP_MS
for _, s in ipairs(SEQUENCES) do
if at < s[2] then
active, self.t = s[1], at
break
end
at = at - s[2] - GAP_MS
if at < 0 then break end
end
end
if active ~= self.active then
self.active = active
if active == "shiba" then self.tail_wag:start() end
if active == "fox" then self.fox_poses:start() end
end
self.tail_wag:update(current_ms)
self.fox_poses:update(current_ms)
end
local function path_at(path, ms)
local frame = ms / FRAME
if frame < path[1][1] or frame > path[#path][1] then return nil end
for i = 2, #path do
local a, b = path[i - 1], path[i]
if frame <= b[1] then
local t = (frame - a[1]) / (b[1] - a[1])
return lerp(a[2], b[2], t), lerp(a[3], b[3], t)
end
end
end
function AttractChara:draw_shiba()
local t = self.t
local y
if t < SHIBA_RISE.ms then
y = lerp(SHIBA_RISE.from, SHIBA_RISE.top, ease_out(t / SHIBA_RISE.ms))
elseif t < SHIBA_RISE.ms + SHIBA_RISE.settle_ms then
y = lerp(SHIBA_RISE.top, SHIBA_REST_Y, ease_in((t - SHIBA_RISE.ms) / SHIBA_RISE.settle_ms))
else
y = lerp(SHIBA_REST_Y, SHIBA_SINK.to, span(t, SHIBA_SINK.start, SHIBA_SINK.start + SHIBA_SINK.ms))
end
local dy = y - SHIBA_REST_Y
-- the tail swings about its base, the bottom-right corner of the image
local angle = t < SHIBA_WAG_START and -70 or self.tail_wag.attribute / 10
tex.draw_texture(self.t_tail, { y = dy, origin = { 128, 152 }, rotation = angle })
local frame = 0
if t >= SHIBA_WAG_START and math.floor((t - SHIBA_WAG_START) / (10 * FRAME)) % 2 == 0 then frame = 1 end
tex.draw_texture(self.t_shiba, { y = dy, frame = frame })
for _, puff in ipairs(PUFFS) do
local u = t - puff[2]
if u >= 0 and u < PUFF_POP_MS + PUFF_FADE_MS then
local scale, fade = 1, 1
if u < PUFF_POP_MS then
scale = lerp(0.3, 1, u / PUFF_POP_MS)
else
local f = (u - PUFF_POP_MS) / PUFF_FADE_MS
fade = 0.96 * (1 - (puff[3] and f or ease_out(f)))
end
tex.draw_texture(self.t_puff, { index = puff[1] - 1, scale = scale, center = true, fade = fade })
end
end
end
function AttractChara:draw_fox()
local t = self.t
local x
if t < FOX_X.out_at then
x = lerp(FOX_X.from, FOX_X.rest, span(t, 0, FOX_X.in_ms))
else
x = lerp(FOX_X.rest, FOX_X.to, span(t, FOX_X.out_at, FOX_X.out_at + FOX_X.out_ms))
end
-- texture.json x is the rest position
tex.draw_texture(self.t_fox, { x = x - FOX_X.rest, frame = math.floor(self.fox_poses.attribute) })
end
function AttractChara:draw_birds()
for _, b in ipairs(BIRDS) do
local x, y = path_at(b[4], self.t)
if x then
local frame = 0
if self.t >= b[3] * FRAME then
frame = math.floor((self.t - b[3] * FRAME) / (b[2] * FRAME)) % 2
end
tex.draw_texture(self.t_birds[b[1]], { x = x, y = y, frame = frame })
end
end
end
function AttractChara:draw()
if self.active == "shiba" then
self:draw_shiba()
elseif self.active == "fox" then
self:draw_fox()
elseif self.active == "birds" then
self:draw_birds()
end
end