Files
PyTaikoGreen/Scripts/background/bg_objects/chibi/base.lua
T
2026-10-10 14:52:27 -04:00

205 lines
8.7 KiB
Lua

-- BaseChibi: a good-hit chibi. It hops in from behind the lane growing from
-- 70% size, lands on its run line and runs 1170px across the don band over 4.5 beats, cycling
-- through a loop of poses. Timed in beats. Texture.json puts each image at the point where chibis
-- appear, before it moves, so every position here is an offset from there.
--
-- spec (a motion, see chibi/sets.lua) plus the character:
-- key, first: texture key and the character's first frame in it (default 0)
-- hop: { from = { x, y }, peak = { x, y }, up = beats to the peak }
-- hop_frames: frame_change steps while hopping (default frame 0)
-- run: { x = where it lands (default -270), y = run line, drift = px down over the run,
-- distance = px run (default 1170) }
-- period: length of the pose loop in beats
-- poses: { beat, frame, x, y, to = { x, y, ease }, sy = h | { from, to }, mirror = true }
-- start_size: size it hops in at (default 0.7; 1 for sets that don't grow)
-- fade_in: fades in over the hop
-- a pose holds its offset until the next one, or glides `to` it; sy squashes
local Timing = require("background/bg_objects/timing")
local chain_moves, frame_change, sum, Loop = Timing.chain_moves, Timing.frame_change, Timing.sum, Timing.Loop
local LIFE = 5 -- beats on screen
local HOP_BEATS = 0.5
local RUN_X, RUN_BEATS, RUN_DISTANCE = -270, 4.5, 1170
local START_SIZE = 0.7
local BaseChibi = {}
BaseChibi.__index = BaseChibi
BaseChibi.HOP = { from = { -418, 21 }, peak = { -324, -37 }, up = 0.325 }
-- Turns a pose table into frame, mirror, x, y and squash tracks for one loop
local function pose_tracks(poses, period, beat, scale, first)
local frames, mirror, x, y, sy = {}, {}, {}, {}, {}
local cx, cy, cs = 0, 0, 1000
for i, p in ipairs(poses) do
local beat_at, frame, px, py = p[1], p[2], p[3] or 0, p[4] or 0
local length = (poses[i + 1] and poses[i + 1][1] or period) - beat_at
table.insert(frames, { frame + first, length })
table.insert(mirror, { p.mirror and 1 or 0, length })
table.insert(x, { 0, px - cx })
table.insert(y, { 0, py - cy })
cx, cy = px, py
if p.to then
table.insert(x, { length, p.to[1] - cx, p.to[3] })
table.insert(y, { length, p.to[2] - cy, p.to[3] })
cx, cy = p.to[1], p.to[2]
else
table.insert(x, { length, 0 })
table.insert(y, { length, 0 })
end
local s0, s1 = 1, 1
if type(p.sy) == "number" then s0, s1 = p.sy, p.sy elseif p.sy then s0, s1 = p.sy[1], p.sy[2] end
table.insert(sy, { 0, s0 * 1000 - cs })
table.insert(sy, { length, (s1 - s0) * 1000 })
cs = s1 * 1000
end
return {
frame = frame_change(frames, beat),
mirror = frame_change(mirror, beat),
x = chain_moves(x, beat, scale),
y = chain_moves(y, beat, scale),
sy = chain_moves(sy, beat, 1),
}
end
function BaseChibi.new(spec, path, folder, bpm, is_2p)
local self = setmetatable({}, BaseChibi)
self:init(spec, path, folder, bpm, is_2p)
return self
end
function BaseChibi:init(spec, path, folder, bpm, is_2p)
local offset = tex.get_skin_config("game_2p_offset")
self.spec = spec
self.path = path
self.folder = folder
self.key = spec.key
self.first = spec.first or 0
self.beat = 60000 / bpm
self.scale = tex.get_screen_scale()
self.y2p = is_2p and offset and offset.y or 0
self.run_x = spec.run and spec.run.x or RUN_X
self.run_y = spec.run and spec.run.y or 1
self._id = tex.get_texture(path .. "/" .. folder .. "/" .. self.key) or 0
self.info = tex.get_texture_info(folder, self.key)
self.ids = {}
end
-- Builds the timeline on the first update, so it starts in step with the controller's clock
function BaseChibi:start()
local spec, beat, scale = self.spec, self.beat, self.scale
local hop = spec.hop or BaseChibi.HOP
local from, peak, up = hop.from, hop.peak, hop.up
local down = HOP_BEATS - up
-- glides across, slows to the peak and drops onto the run line, growing steadily
self.hop_x = chain_moves({ { up, peak[1] - from[1] }, { down, self.run_x - peak[1] } }, beat, scale)
self.hop_y = chain_moves({ { up, peak[2] - from[2], "out" }, { down, self.run_y - peak[2], "in" } }, beat, scale)
self.grow = chain_moves({ { HOP_BEATS - 0.025, (1 - (spec.start_size or START_SIZE)) * 1000 } }, beat, 1)
self.hop_frames = frame_change(spec.hop_frames or { { 0, HOP_BEATS } }, beat)
self.hop_frames:start()
self.run = chain_moves({ { HOP_BEATS, 0 }, { RUN_BEATS, spec.run and spec.run.distance or RUN_DISTANCE } }, beat, scale)
self.drift = chain_moves({ { HOP_BEATS, 0 }, { RUN_BEATS, spec.run and spec.run.drift or 0 } }, beat, scale)
end
local function update_all(list, ms)
for _, m in ipairs(list) do m:update(ms) end
end
function BaseChibi:update(current_time_ms)
if not self.t0 then
self.t0 = current_time_ms
self:start()
end
local t = (current_time_ms - self.t0) / self.beat
self.t = t
self.done = t >= LIFE
self.hopping = t < HOP_BEATS
update_all(self.hop_x, current_time_ms)
update_all(self.hop_y, current_time_ms)
update_all(self.grow, current_time_ms)
update_all(self.run, current_time_ms)
update_all(self.drift, current_time_ms)
self.hop_frames:update(current_time_ms)
if not self.hopping and self.spec.poses then
local spec, beat, scale, first = self.spec, self.beat, self.scale, self.first
self.poses = self.poses or Loop.new(spec.period * beat, function()
local p = pose_tracks(spec.poses, spec.period, beat, scale, first)
p.frame:start()
p.mirror:start()
return p
end)
self.poses:update(current_time_ms)
end
self:update_extra(current_time_ms)
end
function BaseChibi:update_extra(current_time_ms) end
-- Where the chibi is (offset from its texture.json position), its frame and size
function BaseChibi:place()
-- one table per chibi, every field rewritten: place() runs each frame
local at = self._at
if not at then at = {}; self._at = at end
at.mirror, at.rot, at.fade = nil, nil, nil
if self.hopping then
local size = (self.spec.start_size or START_SIZE) + sum(self.grow) / 1000
local hop = self.spec.hop or BaseChibi.HOP
at.x, at.y = hop.from[1] + sum(self.hop_x) / self.scale, hop.from[2] + sum(self.hop_y) / self.scale
at.frame, at.sx, at.sy = self.first + self.hop_frames.attribute, size, size
at.fade = self.spec.fade_in and self.t / HOP_BEATS or nil
return at
end
at.x, at.y = self.run_x + sum(self.run) / self.scale, self.run_y + sum(self.drift) / self.scale
at.frame, at.sx, at.sy = self.first, 1, 1
local p = self.poses and self.poses.anims
if p then
at.x, at.y = at.x + sum(p.x) / self.scale, at.y + sum(p.y) / self.scale
at.frame = p.frame.attribute
at.sy = sum(p.sy) / 1000 + 1
at.mirror = p.mirror.attribute == 1
end
return at
end
-- Draws texture `key` (default the character's) with its centre moved by x, y (unscaled px),
-- sized sx, sy about the centre and turned `rot` degrees about it
-- draw_texture reads these straight away, so every chibi shares one pair
local PARAMS, ORIGIN = {}, { 0, 0 }
function BaseChibi:draw_image(at, key)
local id, info = self._id, self.info
if key then
id = self.ids[key] or tex.get_texture(self.path .. "/" .. self.folder .. "/" .. key) or 0
self.ids[key] = id
info = tex.get_texture_info(self.folder, key)
end
if not (info and info.width) then return end -- a key the folder doesn't have
local s = self.scale
local w, h = info.width, info.height
local W, H = w * (at.sx or 1), h * (at.sy or 1)
ORIGIN[1], ORIGIN[2] = W / 2, H / 2
PARAMS.frame = at.frame or 0
PARAMS.x = at.x * s + w / 2
PARAMS.y = at.y * s + h / 2 + self.y2p
PARAMS.x2 = W - w
PARAMS.y2 = H - h
PARAMS.origin = ORIGIN
PARAMS.rotation = at.rot or 0
PARAMS.mirror = at.mirror and "horizontal" or nil
PARAMS.fade = at.fade
PARAMS.color = nil -- donbg's dark wrapper sets one on whatever table it is handed
tex.draw_texture(id, PARAMS)
end
function BaseChibi:draw()
if not self.t0 then return end
self:draw_image(self:place())
end
-- Adds a good-hit chibi to a ChibiController. cls defaults to BaseChibi; folder to "chibi_1"
function BaseChibi.add(controller, spec, path, player_num, cls, folder)
cls = cls or BaseChibi
table.insert(controller.chibis, cls.new(spec, path, folder or "chibi_1", controller.bpm, player_num == 2))
end
return BaseChibi