-- 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