Files
PyTaikoGreen/Scripts/song_select/battle_story.lua
T
2026-11-14 16:28:08 -05:00

174 lines
5.9 KiB
Lua

BattleStory = {}
BattleStory.__index = BattleStory
local S = tex.get_screen_scale()
local FRAME_MS = 1000 / 60
local BLACK = { 0, 0, 0, 255 }
local FADE_FRAMES = 15
local FONT = "FOT-Seurat_Pro_B.otf"
local SCENES = "song_select/battle_story/"
local PLAYERS = {
chat = require(SCENES .. "chat"), intro = require(SCENES .. "intro"), stage = require(SCENES .. "stage"),
boss = require(SCENES .. "boss"), reward = require(SCENES .. "reward"),
}
local MUSIC = { main = "battle_story_bgm", boss = "battle_story_bgm_boss", info2 = "battle_story_bgm_info2",
info3 = "battle_story_bgm_info3", info4 = "battle_story_bgm_info4", info5 = "battle_story_bgm_info5",
prologue = "battle_story_bgm_prologue" }
local function load_scene(id)
local ok, scene = pcall(require, SCENES .. id)
if ok and type(scene) == "table" and PLAYERS[scene.kind] then return scene end
end
-- ids: the story scenes Hiroba says are due, in order; ones without a scene file are skipped and stay unseen
function BattleStory.new(ids)
local self = setmetatable({}, BattleStory)
self.ids, self.sizes = {}, {}
self.queue, self.watched = {}, {}
for _, id in ipairs(ids) do
local scene = load_scene(id)
if scene then self.queue[#self.queue + 1] = { id = id, scene = scene } end
end
if #self.queue > 0 then
tex.load_folder("song_select", "battle_story")
end
self.index = 0
self.closing = false
return self
end
function BattleStory:next_scene(ms)
self.index = self.index + 1
local entry = self.queue[self.index]
self.player = entry and PLAYERS[entry.scene.kind].new(self, entry.scene)
self.scene_ms, self.closing = ms, false
end
function BattleStory:update(ms)
self.ms = ms
if self.index == 0 then self:next_scene(ms) end
if not self.player then return end
local f = (ms - self.scene_ms) / FRAME_MS
if self.closing then
if (ms - self.closing) / FRAME_MS >= FADE_FRAMES then
self.watched[#self.watched + 1] = self.queue[self.index].id
self:next_scene(ms)
if not self.player then self:finish() end
end
return
end
self.player:update(f)
if self.player:done() then self.closing = ms end
end
function BattleStory:finish()
self:music("stop")
tex.unload_folder("game", "battle_stage")
tex.unload_folder("game", "enso_battle")
end
function BattleStory:input(action)
if self.player and not self.closing and self.ms then
self.player:input(action, (self.ms - self.scene_ms) / FRAME_MS)
end
end
function BattleStory:draw()
if not self.player then return end
self.player:draw((self.ms - self.scene_ms) / FRAME_MS)
local black = math.max(0, 1 - (self.ms - self.scene_ms) / FRAME_MS / FADE_FRAMES)
if self.closing then black = math.min(1, (self.ms - self.closing) / FRAME_MS / FADE_FRAMES) end
if black > 0 then
tex.draw_rect(0, 0, tex.get_screen_width(), tex.get_screen_height(), 0, 0, 0, 255 * black)
end
end
-- name: a MUSIC key or "stop"; the playing track keeps going when asked for again
function BattleStory:music(name)
if name == self.track then return end
if self.track and self.track ~= "stop" then audio.stop_sound(MUSIC[self.track]) end
self.track = name
if MUSIC[name] then audio.play_sound(MUSIC[name], "music", true) end
end
function BattleStory:is_last()
return self.index == #self.queue
end
function BattleStory:is_done()
return self.index > 0 and not self.player
end
function BattleStory:played()
return self.watched
end
-- helpers for the scene players
-- text = { ja = ..., en = ... }; the player's language, then English, then Japanese
function BattleStory.pick(t)
return t[tex.language()] or t.en or t.ja or ""
end
function BattleStory.text(str, size, color, outline, thickness)
return text.create_raw_text(str, math.floor(size * S), color, outline or color, false, thickness or 0, 0, 1, FONT)
end
function BattleStory:id(path)
local id = self.ids[path]
if id == nil then
id = tex.get_texture(path)
self.ids[path] = id
end
return id
end
function BattleStory:size(folder, key)
local path = folder .. "/" .. key
local info = self.sizes[path]
if info == nil then
info = tex.get_texture_info(folder, key) or false
self.sizes[path] = info
end
return info and info.width or 0, info and info.height or 0
end
-- Draws song_select/battle_story/`key`. Without o.at it sits at its texture.json spot moved by (dx, dy); with o.at
-- (dx, dy) is where its anchor goes. o: sx, sy (scale about the anchor ax, ay, fractions of the size), fade, add,
-- mirror (flipped in place).
function BattleStory:put(key, dx, dy, o)
o = o or {}
local w, h = self:size("battle_story", key)
local sx, sy = o.sx or o.s or 1, o.sy or o.s or 1
local ax, ay = o.ax or 0.5, o.ay or 0.5
local x, y = dx, dy
if not o.at then x, y = dx + w * ax, dy + h * ay end
tex.draw_texture(self:id("song_select/battle_story/" .. key), {
x = x * S, y = y * S,
x2 = w * S * (sx - 1), y2 = h * S * (sy - 1),
origin = { w * S * sx * ax, h * S * sy * ay },
fade = o.fade or 1,
blend = o.add and "additive" or nil,
mirror = o.mirror and "horizontal" or nil,
})
end
-- the "hit to continue" pill; t: a text of battle_story_end
function BattleStory:draw_hint(t)
local icon, gap, pad = 40, 8, 10
local w = (pad + icon + gap + 16) * S + t.width
local x, y, h = 640 * S - w / 2, 690, 36
tex.draw_rounded_rect(x, (y - h / 2) * S, w, h * S, 1, BLACK, 0, BLACK)
self:put("hint_face", x / S + pad + icon / 2, y, { at = true, s = icon / 55 })
t:draw({ x = x + (pad + icon + gap) * S, y = y * S - t.height / 2 })
end
function BattleStory:backdrop(key)
tex.draw_texture(self:id("game/battle_stage/" .. key), {})
end
function BattleStory.sound(name)
name = "battle_story_" .. name
if audio.has_sound(name) then audio.play_sound(name, "sound") end
end