338 lines
13 KiB
Lua
338 lines
13 KiB
Lua
require("entry/campaign_menu")
|
|
require("campaign/smart_scan")
|
|
|
|
Kinotake = {}
|
|
Kinotake.__index = Kinotake
|
|
|
|
local S = tex.get_screen_scale()
|
|
local DIR = "campaign/kinotake"
|
|
local F = 1000 / 60
|
|
local LEFT, RIGHT, DECIDE = 1, 2, 3
|
|
local KINOKO, TAKENOKO = 0, 1
|
|
local COSTUME = { [KINOKO] = 105, [TAKENOKO] = 106 }
|
|
|
|
local TITLE_F = 229
|
|
local CURSOR_X = { [KINOKO] = -150, [TAKENOKO] = 145 }
|
|
local SIDE_WAIT_F, SIDE_FADE_F, MOVE_F = 60, 20, 5
|
|
local SIDE_SELECT_MS = 20000
|
|
local BALLOON_WAIT_F, BALLOON_F = 120, 129
|
|
local GIFT_F = 220
|
|
|
|
local function clamp01(v) return math.max(0.0, math.min(1.0, v)) end
|
|
local function ramp(t, at, len) return clamp01((t - at) / len) end
|
|
local function ease_in(u) return u * u end
|
|
local function ease_out(u) return u * (2 - u) end
|
|
|
|
-- { frame, value, ease of the segment ending here } -> value at frame f
|
|
local function track(f, keys)
|
|
if f <= keys[1][1] then return keys[1][2] end
|
|
for i = 2, #keys do
|
|
local a, b = keys[i - 1], keys[i]
|
|
if f < b[1] then
|
|
local u = (f - a[1]) / (b[1] - a[1])
|
|
if b[3] == "out" then u = ease_out(u) elseif b[3] == "in" then u = ease_in(u) end
|
|
return a[2] + (b[2] - a[2]) * u
|
|
end
|
|
end
|
|
return keys[#keys][2]
|
|
end
|
|
|
|
local function play(name, kind)
|
|
if audio.has_sound(name) then audio.play_sound(name, kind or (name:find("_vo_") and "voice" or "sound")) end
|
|
end
|
|
|
|
function Kinotake.new(seats)
|
|
local self = setmetatable({}, Kinotake)
|
|
self.tex = {}
|
|
for _, key in ipairs({
|
|
"bg", "cover", "logo", "banner", "logo_flash", "banner_flash", "burst_l", "burst_r", "copyright", "board",
|
|
"cloud_kinoko", "cloud_takenoko", "frame_1p", "frame_2p", "half_1p", "half_2p", "glow", "glow_half",
|
|
"bubble_1p", "bubble_2p", "bubble_1p_duet", "bubble_2p_duet", "flash", "flash_1p_duet", "flash_2p_duet",
|
|
"balloon", "gift_note", "gift_kid", "gift_kinoko", "gift_takenoko",
|
|
}) do
|
|
local id = tex.get_texture(DIR .. "/" .. key) -- loads the folder, so before the info lookup
|
|
local info = tex.get_texture_info(DIR:match("[^/]+$"), key) -- textures are keyed by their own folder
|
|
self.tex[key] = { id = id, w = info and info.x2[1] or 0,
|
|
h = info and info.y2[1] or 0 }
|
|
end
|
|
self.seats = {}
|
|
for _, p in ipairs(seats or { 1 }) do self.seats[#self.seats + 1] = p end
|
|
self.cursors = {}
|
|
self.team = {}
|
|
self.reward = -1
|
|
self.now = tex.get_current_ms()
|
|
play("kinotake_bgm", "music")
|
|
self:enter("title")
|
|
return self
|
|
end
|
|
|
|
-- draws `t` scaled by sx / sy about its centre, moved by dx / dy (unscaled pixels)
|
|
function Kinotake:put(t, opts)
|
|
opts = opts or {}
|
|
local sx, sy = opts.sx or 1, opts.sy or opts.sx or 1
|
|
tex.draw_texture(t.id, {
|
|
x = t.w * (1 - sx) / 2 + (opts.dx or 0) * S, y = t.h * (1 - sy) / 2 + (opts.dy or 0) * S,
|
|
x2 = t.w * (sx - 1), y2 = t.h * (sy - 1), fade = opts.fade, mirror = opts.mirror,
|
|
})
|
|
end
|
|
|
|
function Kinotake:enter(phase)
|
|
self.phase = phase
|
|
self.t0 = self.now
|
|
self.cue_f = -1
|
|
if phase == "select" then
|
|
self.menu = CampaignMenu.new({ "kinotake", "recog", "skip" }, { dir = DIR, sound = "kinotake_vo_sel_which" })
|
|
elseif phase == "recog" then
|
|
self.recog_team = math.random(0, 1)
|
|
self.reward = COSTUME[self.recog_team]
|
|
local function k(key) return DIR .. "/" .. key end
|
|
self.scan = SmartScan.new("v3c", {
|
|
tex = {
|
|
bg = k("bg"), title_bg = k("cover"), side = k("recog_side"), guide = k("recog_guide"),
|
|
keep_box = k("recog_keep_box"), recog_keep_text = k("recog_keep_text"), prize = k("prize"),
|
|
flowers_kinoko = k("flowers_kinoko"), flowers_takenoko = k("flowers_takenoko"),
|
|
},
|
|
skip_title = true, gift_ms = GIFT_F * F, draw_gift = function(_, t) self:draw_gift(t) end,
|
|
keep_tex = "recog_keep_text", flowers = { "flowers_kinoko", "flowers_takenoko" },
|
|
prize = self.recog_team, prize_text = "kinotake_costume_" .. self.recog_team, reward = self.reward,
|
|
bgm = false, sounds = { smart_vo_campaign = false, smart_vo_moraeru = "kinotake_vo_exp_recog" },
|
|
})
|
|
elseif phase == "side" then
|
|
for i, p in ipairs(self.seats) do
|
|
local index = self.recog_team or (i == 1 and KINOKO or TAKENOKO)
|
|
self.cursors[i] = { player = p, index = index, from = index, moved = -MOVE_F * F }
|
|
end
|
|
self.all_decided = nil
|
|
self.side_t0 = self.now
|
|
play("kinotake_vo_which")
|
|
elseif phase == "balloon" then
|
|
play("kinotake_vo_entered")
|
|
end
|
|
end
|
|
|
|
function Kinotake:frames() return (self.now - self.t0) / F end
|
|
|
|
function Kinotake:cue(f, name)
|
|
if f > self.cue_f and f <= self:frames() then play(name) end
|
|
end
|
|
|
|
function Kinotake:update(current_ms)
|
|
self.now = current_ms
|
|
if self.done then return end
|
|
local f = self:frames()
|
|
if self.phase == "title" then
|
|
self:cue(1, "kinotake_se_popup")
|
|
self:cue(35, "kinotake_se_logoshine")
|
|
self:cue(54, "kinotake_vo_title")
|
|
self:cue(100, "kinotake_se_popup")
|
|
self:cue(170, "kinotake_se_popup")
|
|
if f >= TITLE_F then self:enter("select") end
|
|
elseif self.phase == "select" then
|
|
self.menu:update(current_ms)
|
|
if self.menu:is_done() then
|
|
local choice = self.menu:get_choice()
|
|
if choice == "skip" then self:finish() else self:enter(choice == "recog" and "recog" or "side") end
|
|
end
|
|
elseif self.phase == "recog" then
|
|
self.scan:update(current_ms)
|
|
if self.scan:is_done() then
|
|
if self.scan:failed() then self.recog_team, self.reward = nil, -1 end -- no camera: pick a side as usual
|
|
self:enter("side")
|
|
end
|
|
elseif self.phase == "side" then
|
|
if self.menu then self.menu:update(current_ms) end
|
|
if not self.all_decided and current_ms - self.t0 >= (SIDE_WAIT_F + SIDE_FADE_F) * F + SIDE_SELECT_MS then
|
|
for _, c in ipairs(self.cursors) do self:decide(c) end
|
|
end
|
|
if self.all_decided and current_ms - self.all_decided >= BALLOON_WAIT_F * F then self:enter("balloon") end
|
|
elseif self.phase == "balloon" then
|
|
self:cue(19, "kinotake_se_breakout")
|
|
if f >= BALLOON_F then self:finish() end
|
|
end
|
|
self.cue_f = f
|
|
end
|
|
|
|
function Kinotake:finish()
|
|
self.done = true
|
|
self:close()
|
|
end
|
|
|
|
function Kinotake:close()
|
|
audio.stop_sound("kinotake_bgm")
|
|
if self.scan then self.scan:close() end
|
|
end
|
|
|
|
function Kinotake:cursor_of(player)
|
|
for _, c in ipairs(self.cursors) do
|
|
if c.player == player then return c end
|
|
end
|
|
end
|
|
|
|
function Kinotake:decide(c)
|
|
if c.decided then return end
|
|
c.decided = self.now
|
|
self.team[c.player] = c.index
|
|
for _, o in ipairs(self.cursors) do
|
|
if not o.decided then return end
|
|
end
|
|
self.all_decided = self.now
|
|
end
|
|
|
|
function Kinotake:input(action, player)
|
|
if self.done then return false end
|
|
if self.phase == "select" then
|
|
return self.menu:input(action)
|
|
elseif self.phase == "recog" then
|
|
return action == DECIDE and self.scan:input(action)
|
|
elseif self.phase ~= "side" or self:frames() < SIDE_WAIT_F + SIDE_FADE_F or self.all_decided then
|
|
return false
|
|
end
|
|
local c = self:cursor_of(player or self.seats[1]) or self.cursors[1]
|
|
if not c or c.decided or self.now - c.moved < MOVE_F * F then return false end
|
|
if action == DECIDE then
|
|
self:decide(c)
|
|
return true
|
|
end
|
|
local to = action == LEFT and KINOKO or TAKENOKO
|
|
if to == c.index then return false end
|
|
c.from, c.index, c.moved = c.index, to, self.now
|
|
return true
|
|
end
|
|
|
|
function Kinotake:is_done() return self.done == true end
|
|
function Kinotake:get_team(player) return self.team[player] or -1 end
|
|
function Kinotake:get_reward() return self.reward end
|
|
|
|
function Kinotake:draw_backdrop()
|
|
tex.draw_texture(self.tex.cover.id)
|
|
tex.draw_texture(self.tex.bg.id)
|
|
end
|
|
|
|
function Kinotake:draw_title(f)
|
|
local tx = self.tex
|
|
self:draw_backdrop()
|
|
local lift = track(f, { { 0, 700 }, { 20, 118.15, "out" }, { 25, 135, "in" }, { 75, 135 }, { 87, 0, "out" } })
|
|
self:put(tx.logo, { dy = lift })
|
|
self:put(tx.banner, { dy = lift })
|
|
if f >= 25 and f < 54 then
|
|
local s, a = 1, ramp(f, 25, 10)
|
|
if f >= 36 then
|
|
local u = ease_out(ramp(f, 36, 18))
|
|
s, a = 1.328 + 2.872 * u, 0.9 * (1 - u)
|
|
end
|
|
self:put(tx.logo_flash, { dy = lift, sx = s, fade = a })
|
|
self:put(tx.banner_flash, { dy = lift, sx = s, fade = a })
|
|
end
|
|
for _, b in ipairs({ { tx.burst_l, 74 }, { tx.burst_r, 145 } }) do
|
|
local u = f - b[2]
|
|
if u >= 0 then
|
|
local dy = track(u, { { 0, 548.15 }, { 13, -16.85, "out" }, { 21, 9, "in" }, { 26, 0 } })
|
|
local sy = track(u, { { 13, 1 }, { 21, 0.95, "in" }, { 26, 1 } })
|
|
self:put(b[1], { dy = dy, sy = sy, sx = 1 })
|
|
end
|
|
end
|
|
tex.draw_texture(tx.copyright.id, { fade = ramp(f, 87, 6) })
|
|
end
|
|
|
|
function Kinotake:draw_gift(t)
|
|
local tx, f = self.tex, t / F
|
|
local out = 1 - ramp(f, 214, 5)
|
|
self:put(tx.gift_note, { fade = ramp(f, 10, 9) * out })
|
|
if f >= 5 then
|
|
local dy = track(f, { { 4, 490 }, { 15, -44.05, "out" }, { 23, -11.4, "in" }, { 28, 7.65, "out" }, { 32, 0, "in" } })
|
|
self:put(tx.gift_kid, { dy = dy, fade = out })
|
|
end
|
|
-- the boxes pop up, then breathe out of step
|
|
for i, b in ipairs({ { tx.gift_kinoko, 10 }, { tx.gift_takenoko, 11 } }) do
|
|
local u = f - b[2]
|
|
if u >= 0 then
|
|
local dy = track(u, { { 0, 498 }, { 9, -8 }, { 13, 0 } })
|
|
local p = u < 14 and 0 or ((u - 14) % 40) / 40
|
|
local s = p < 0.5 and p * 2 or 1 - (p - 0.5) * 2
|
|
if i == 1 then s = 0.95 + 0.05 * s else s = 1 - 0.05 * s end
|
|
self:put(b[1], { dy = dy, sx = s, fade = out })
|
|
end
|
|
end
|
|
end
|
|
|
|
function Kinotake:draw_cloud(t, f)
|
|
local p = (f % 40)
|
|
local s = p < 17 and 1 + 0.05 * ease_out(p / 17) or p < 24 and 1.05 or 1.05 - 0.05 * ease_in((p - 24) / 16)
|
|
self:put(t, { sx = s })
|
|
end
|
|
|
|
function Kinotake:draw_cursor(c, f, duet)
|
|
local tx = self.tex
|
|
local wait = SIDE_WAIT_F
|
|
if f < wait then return end
|
|
local fade = ramp(f, wait, SIDE_FADE_F)
|
|
local moved = (self.now - c.moved) / (MOVE_F * F)
|
|
local x = CURSOR_X[c.from] + (CURSOR_X[c.index] - CURSOR_X[c.from]) * clamp01(moved)
|
|
local flash = 0
|
|
if c.decided then
|
|
local d = (self.now - c.decided) / F
|
|
flash = ramp(d, 0, 15)
|
|
fade = fade * (1 - ramp(d, 16, 13))
|
|
if fade <= 0 then return end
|
|
end
|
|
local p1 = c.player == 1
|
|
local suffix = p1 and "1p" or "2p"
|
|
local frame = duet and tx["half_" .. suffix] or tx["frame_" .. suffix]
|
|
local bubble = duet and tx["bubble_" .. suffix .. "_duet"] or tx["bubble_" .. suffix]
|
|
local flash_tex = duet and tx["flash_" .. suffix .. "_duet"] or tx.flash
|
|
self:put(bubble, { dx = x, fade = fade })
|
|
if flash > 0 then self:put(flash_tex, { dx = x, fade = fade * flash }) end
|
|
self:put(frame, { dx = x, fade = fade })
|
|
-- white rim glow: in 467 ms, out 500 ms, every 2 s
|
|
local g = ((self.now - self.side_t0) % 2000)
|
|
local glow = g < 467 and 0.58 * g / 467 or g < 967 and 0.6 * (1 - (g - 467) / 500) or 0
|
|
self:put(duet and tx.glow_half or tx.glow, { dx = x, fade = fade * glow,
|
|
mirror = duet and p1 and "horizontal" or nil })
|
|
end
|
|
|
|
function Kinotake:draw_side()
|
|
local tx = self.tex
|
|
local f = (self.now - self.side_t0) / F
|
|
self:draw_backdrop()
|
|
self:put(tx.board)
|
|
self:draw_cloud(tx.cloud_kinoko, f)
|
|
self:draw_cloud(tx.cloud_takenoko, f)
|
|
local a, b = self.cursors[1], self.cursors[2]
|
|
local duet = a and b and a.index == b.index
|
|
for i = #self.cursors, 1, -1 do self:draw_cursor(self.cursors[i], f, duet) end
|
|
if self.menu then self.menu:draw_hint(1) end
|
|
end
|
|
|
|
function Kinotake:draw_balloon(f)
|
|
local s, a = 1, 1
|
|
if f < 19 then
|
|
s, a = 2 - f / 19, f / 19
|
|
elseif f < 23 then
|
|
s = 1.013 + 0.037 * ramp(f, 20, 3)
|
|
elseif f < 29 then
|
|
s = 1.042 - 0.042 * ramp(f, 24, 5)
|
|
end
|
|
self:put(self.tex.balloon, { sx = s, fade = a })
|
|
if f >= 65 then
|
|
tex.draw_rect(0, 0, tex.get_screen_width(), tex.get_screen_height(), 0, 0, 0, 255 * ramp(f, 65, 60))
|
|
end
|
|
end
|
|
|
|
function Kinotake:draw()
|
|
if self.done then return end
|
|
local f = self:frames()
|
|
if self.phase == "title" then
|
|
self:draw_title(f)
|
|
elseif self.phase == "select" then
|
|
self:draw_backdrop()
|
|
self.menu:draw()
|
|
elseif self.phase == "recog" then
|
|
self.scan:draw()
|
|
elseif self.phase == "side" then
|
|
self:draw_side()
|
|
elseif self.phase == "balloon" then
|
|
self:draw_side()
|
|
self:draw_balloon(f)
|
|
end
|
|
end
|