AIIntro = {} AIIntro.__index = AIIntro local S = tex.get_screen_scale() local F = 1000 / 60 local LEFT, RIGHT, DECIDE = 1, 2, 3 local SKIP_WAIT, FORCED_SKIP = 30, 1200 local INPUT_WAIT = 60 local OUT_FRAMES = 30 local OX, OY = 640, 324 local function clamp01(v) return math.max(0, math.min(1, v)) end local function ramp(f, at, len) return clamp01((f - at) / len) end local function lerp(a, b, u) return a + (b - a) * u end -- rows: { from, to, scale_from, scale_to, y_from, y_to } local function track(rows, f) if rows then for _, r in ipairs(rows) do if f <= r[2] then local u = r[2] > r[1] and clamp01((f - r[1]) / (r[2] - r[1])) or 1 return lerp(r[3], r[4], u), lerp(r[5], r[6], u) end end end return 1, 0 end local DON_HOP = { { 0, 3, 1, 0.988, 0, 2 }, { 4, 6, 0.992, 1, 1.3, 0 }, { 7, 16, 1, 1, -7.8, -78 }, { 17, 27, 1, 1, -70.9, 0 }, { 28, 30, 0.996, 0.988, 0.65, 2 }, { 31, 33, 0.992, 1, 1.3, 0 }, } local DON_JUMP = { { 0, 2, 1, 0.987, 0, 4 }, { 3, 12, 0.98, 0.998, 6, -140 }, { 13, 23, 1, 1, -156, -14 } } local AIDON_HOP = { { 0, 3, 1, 0.965, 0, 7 }, { 4, 9, 0.983, 1.075, -18, -144 }, { 10, 15, 1.057, 0.965, -119, 7 }, { 16, 19, 0.974, 1, 5, 0 }, } local AIDON_TRIPLE = { { 0, 3, 1, 0.965, 0, 7 }, { 4, 9, 0.983, 1.075, -18, -144 }, { 10, 15, 1.057, 0.965, -119, 7 }, { 16, 25, 0.974, 1.075, 5, -144 }, { 26, 31, 1.057, 0.965, -119, 7 }, { 32, 39, 0.974, 1.075, 5, -144 }, { 40, 45, 1.057, 0.965, -119, 7 }, { 46, 49, 0.974, 1, 5, 0 }, } local MAKINA_HOP = { { 0, 9, 1, 1, 0, -30 }, { 10, 19, 1, 1, -27, 0 } } local MAKINA_DOUBLE = { { 0, 9, 1, 1, 0, -30 }, { 10, 19, 1, 1, -27, 0 }, { 20, 28, 1, 1, -3, -30 }, { 29, 38, 1, 1, -27, 0 }, } local AIDON_BIG_JUMP = { { 0, 79, 1, 1, 0, 0 }, { 80, 83, 0.977, 0.906, -0.25, -1 }, { 84, 87, 0.93, 1, -0.8, 0 }, { 88, 100, 1.005, 1.07, -10, -132 }, { 101, 114, 1.065, 1, -122.5, 0 }, { 115, 118, 0.977, 0.906, -0.25, -1 }, { 119, 121, 0.938, 1, -0.65, 0 }, } local PHRASES = { { bubble = "bubble_don", line = "line_don_where", se = "se_absurd", don = { "don_idle", DON_HOP }, bgm = true }, { bubble = "bubble_aidon", line = "line_aidon_awake", aidon = AIDON_HOP }, { bubble = "bubble_makina", line = "line_makina_hello", se = "se_norm", vo = "vo_makina_hello", makina = { "open", MAKINA_HOP } }, { bubble = "bubble_don", line = "line_don_dream", se = "se_absurd" }, { bubble = "bubble_makina", line = "line_makina_not_dream", se = "se_norm", makina = { "smile", MAKINA_DOUBLE } }, { bubble = "bubble_makina", line = "line_makina_future", se = "se_norm", makina = { "blink" } }, { bubble = "bubble_makina", line = "line_makina_sudden", se = "se_norm" }, { bubble = "bubble_makina", line = "line_makina_challenge", se = "se_norm" }, { bubble = "bubble_don", line = "line_don_strongest", se = "se_don_jump", don = { "don_cheer", DON_JUMP } }, { bubble = "bubble_makina", line = "line_makina_yes", se = "se_norm", don = { "don_idle" }, makina = { "glad", MAKINA_HOP } }, { bubble = "bubble_makina", line = "line_makina_learning", se = "se_norm" }, { bubble = "bubble_makina", line = "line_makina_close_battle", se = "se_norm" }, { bubble = "bubble_don", line = "line_don_confident", se = "se_norm", don = { "don_cheer", DON_JUMP } }, { bubble = "bubble_aidon", line = "line_aidon_greeting", se = "se_norm", vo = "vo_aidon_thanks", aidon = AIDON_TRIPLE }, { bubble = "bubble_makina", line = "line_makina_lets_go", se = "se_norm", vo = "vo_makina_thanks", makina = { "glad", MAKINA_HOP }, stop_bgm = true }, } local PAGE_END = { [0] = 0, 168, 183, 319, 0, 88, 209 } local LAST_PAGE = 6 local DUEL_FRAMES = { 0, 1, 2, 3, 2, 1 } -- bright = starts lit, fades out first local SPARKLES = { { "sparkle_green", -111, -198, true }, { "sparkle_green", 226, 202, true }, { "sparkle_yellow", -261, 179 }, { "sparkle_green", 247, -203 }, { "sparkle_blue", -258, -142 }, { "sparkle_pink", 350, -171, true }, { "sparkle_pink", -329, -8 }, { "sparkle_pink", 305, 146 }, { "sparkle_yellow_small", -332, -176, true }, { "sparkle_yellow_small", -373, 131, true }, { "sparkle_yellow_small", 365, 27 }, { "sparkle_blue_small", 150, 142, true, 1.2 }, } function AIIntro.new() local self = setmetatable({}, AIIntro) tex.load_folder("song_select", "ai_intro") self.ids, self.sizes = {}, {} self.draw_background = require("song_select/ai_background").new() self.hint_page = text.create_text("ai_intro_hint_page", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 5, 2) self.hint_end = text.create_text("ai_intro_hint_end", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 5, 2) self.now = tex.get_current_ms() self.mode = "story" self.story_t0 = self.now self.don = { "don_idle" } self:show_phrase(1) return self end function AIIntro:frame_since(ms) return (self.now - ms) / F end function AIIntro:play(name, preset) name = "ai_intro_" .. name if audio.has_sound(name) then audio.play_sound(name, preset or (name:find("_vo_") and "voice" or "sound")) end end function AIIntro:show_phrase(n) local p = PHRASES[n] self.phrase, self.phrase_t0 = n, self.now if p.don then self.don, self.don_t0 = p.don, self.now end if p.aidon then self.aidon, self.aidon_t0 = p.aidon, self.now end if p.makina then self.makina, self.makina_t0 = p.makina, self.now end if p.se then self:play(p.se) end if p.vo then self:play(p.vo) end if p.bgm then audio.play_sound("ai_intro_story_bgm", "music", true) end if p.stop_bgm then audio.stop_sound("ai_intro_story_bgm") end end function AIIntro:start_tutorial() self.mode = "tutorial" self.tutorial_t0 = self.now self:play("vo_rule") audio.play_sound("ai_intro_tutorial_bgm", "music", true) self:show_page(0) end function AIIntro:show_page(n) self.page, self.page_t0, self.rankup_played = n, self.now, false end function AIIntro:page_frame() return math.min(self:frame_since(self.page_t0), PAGE_END[self.page]) end function AIIntro:next_phrase() if self.phrase < #PHRASES then self:show_phrase(self.phrase + 1) return end audio.play_sound("don", "sound") self:start_tutorial() end function AIIntro:update(current_ms) self.now = current_ms if self.mode == "story" then if self:frame_since(self.phrase_t0) >= FORCED_SKIP then self:next_phrase() end elseif self.mode == "tutorial" and self.page == 5 and not self.rankup_played and self:page_frame() >= PAGE_END[5] then self.rankup_played = true self:play("vo_rankup") end end function AIIntro:input_story(action) if self:frame_since(self.phrase_t0) < SKIP_WAIT then return end local last = self.phrase == #PHRASES if (action == DECIDE) == last then self:next_phrase() end end function AIIntro:input_tutorial(action) if self:frame_since(self.tutorial_t0) < INPUT_WAIT then return end if action == DECIDE then if self.page == LAST_PAGE then audio.play_sound("don", "sound") audio.stop_sound("ai_intro_tutorial_bgm") self.mode, self.out_t0 = "out", self.now end return end local ended = self:page_frame() >= PAGE_END[self.page] if action == LEFT and self.page == 0 then return end if action == RIGHT and self.page == LAST_PAGE and ended then return end audio.play_sound("kat", "sound") if not ended then self.page_t0 = self.now - PAGE_END[self.page] * F -- first hit finishes the page's animation else self:show_page(self.page + (action == LEFT and -1 or 1)) end end function AIIntro:input(action) if self.mode == "story" then self:input_story(action) elseif self.mode == "tutorial" then self:input_tutorial(action) end end function AIIntro:is_done() return self.mode == "out" and self:frame_since(self.out_t0) >= OUT_FRAMES end function AIIntro:put(key, cx, cy, o) o = o or {} local id = self.ids[key] if id == nil then id = tex.get_texture("song_select/ai_intro/" .. key) self.ids[key] = id self.sizes[key] = tex.get_texture_info("ai_intro", key) end local info = self.sizes[key] if not info then return end local sx, sy = o.sx or o.s or 1, o.sy or o.s or 1 local w, h = info.width * S, info.height * S tex.draw_texture(id, { x = cx * S, y = cy * S, x2 = w * (sx - 1), y2 = h * (sy - 1), origin = { w * sx * 0.5, h * sy * 0.5 }, fade = o.fade or 1, frame = o.frame or 0, mirror = o.mirror, blend = o.add and "additive" or nil, }) end function AIIntro:board(key, x, y, o) self:put(key, OX + x, OY + y, o) end local function draw_hint_text(hint, cx, cy, fade) hint:draw({ x = (cx - hint.width / (2 * S)) * S, y = (cy - hint.height / (2 * S)) * S, fade = fade }) end -- drum with blinking rims; sides = "left", "right", "both" function AIIntro:draw_rim_hint(ox, oy, f, sides, no_left_arrow) local fade = ramp(f, 45, 14) if fade <= 0 then return end local g = (f - 45) % 120 local glow = g < 30 and g / 30 or math.max(1 - (g - 30) / 30, 0) local arrow = clamp01(1 - g / 60) local dx = clamp01(g / 60) * 7 local cx, cy = ox - 132 + 28, oy - 29 + 28 self:put("drum_rim", cx, cy, { fade = fade }) if sides ~= "right" then self:put("drum_rim_glow", cx - 12, cy, { fade = fade * glow }) if not no_left_arrow then self:put("drum_rim_arrow", cx - 27 - dx, cy, { fade = fade * arrow }) end end if sides ~= "left" then self:put("drum_rim_glow", cx + 12, cy, { fade = fade * glow, mirror = "horizontal" }) self:put("drum_rim_arrow", cx + 27 + dx, cy, { fade = fade * arrow, mirror = "horizontal" }) end draw_hint_text(self.hint_page, ox + 52, oy - 2, fade) end function AIIntro:draw_decide_hint(ox, oy, f) local fade = ramp(f, 9, 15) if fade <= 0 then return end local g = (f - 9) % 60 local blink = g < 30 and 1 - g / 30 or (g - 30) / 30 local flash = (f - 9) % 120 flash = flash < 30 and flash / 30 or (flash < 60 and 1 - (flash - 30) / 30 or 0) self:put("hint_glow", ox + 120, oy - 3, { fade = fade * blink }) self:put("hint_glow_round", ox - 43, oy - 1, { fade = fade * blink }) draw_hint_text(self.hint_end, ox + 87, oy - 1, fade) -- the baked text sits left of its glow atlas self:put("drum_decide", ox - 43, oy - 1, { fade = fade }) self:put("drum_decide_flash", ox - 43, oy - 1, { fade = fade * flash }) end function AIIntro:draw_story() local OY_S = 326 local p = PHRASES[self.phrase] local don_d, don_y = track(self.don[2], self:frame_since(self.don_t0)) self:put(self.don[1], 327, 302 + don_y, { sy = don_d }) if self.aidon then local d, y = track(self.aidon, self:frame_since(self.aidon_t0)) self:put("aidon", 870, 312 + y, { sy = d }) end if self.makina then local f = self:frame_since(self.makina_t0) local _, y = track(self.makina[2], f) local key = "makina_open" if self.makina[1] == "smile" then key = "makina_smile" elseif self.makina[1] == "glad" then key = f < 175 and "makina_glad" or "makina_open" elseif self.makina[1] == "blink" then local b = f % 182 key = (b >= 173 and b < 181) and "makina_blink" or "makina_open" end self:put(key, 1074, OY_S - 36 + y) end self:put(p.bubble, 640, OY_S + 216) self:put(p.line, 640, OY_S + 219) local f = self:frame_since(self.phrase_t0) if self.phrase == #PHRASES then self:draw_decide_hint(993, 671, f) else self:draw_rim_hint(993, 668, self:frame_since(self.story_t0), "both", true) end end local function sparkle_alpha(f, bright) local g = f % 120 local up = g <= 60 and g / 60 or 1 - (g - 60) / 60 return bright and 1 - up or up end function AIIntro:draw_sparkles(ox, oy, f) for _, s in ipairs(SPARKLES) do self:board(s[1], ox + s[2], oy + s[3], { fade = sparkle_alpha(f, s[4]), s = s[5] }) end end function AIIntro:draw_duel(x, y, f) self:board("duel", x, y, { frame = DUEL_FRAMES[math.floor(f / 4) % 6 + 1] }) end function AIIntro:draw_burst(key, x, y, f, fade) if f >= 9 then return end local s = lerp(0.4, 1, clamp01(f)) local a = f < 7 and 1 or lerp(0.5, 0, clamp01(f - 7)) local frame = f < 3 and 0 or (f < 5 and 1 or (f < 7 and 2 or 3)) self:board(key, x, y, { frame = frame, s = s, fade = a * fade, add = true }) end function AIIntro:draw_hit_text(key, x, y, f, big, fades) local from, peak, settle_from, rest = -45, -79, -75.75, -66 if not big then from, peak, settle_from, rest = -35, -59, -55.75, -46 end local ty, a if f < 2 then ty, a = lerp(from, peak, clamp01(f)), lerp(0.5, 1, clamp01(f)) elseif f < 6 then ty, a = lerp(settle_from, rest, (f - 2) / 3), 1 else ty, a = rest, 1 end if fades and f >= 29 then a = lerp(0.832, 0, clamp01((f - 29) / 5)) end self:board(key, x, y + ty, { fade = a }) end function AIIntro:draw_note(y, f, at, stop) if f < at or f > stop + 3 then return end if f <= stop then self:board("note_don", lerp(361, -95, (f - at) / (stop - at)), y) else local u = (f - stop - 1) / 2 self:board("note_don", lerp(-109, -137, u), y, { fade = lerp(0.668, 0, u) }) end end function AIIntro:draw_note_label(y, f, at, stop) if f < at or f > stop + 3 then return end if f <= stop then self:board("note_label", lerp(361, -95, (f - at) / (stop - at)), y) else local u = (f - stop - 1) / 2 self:board("note_label", lerp(-109, -137, u), y, { fade = lerp(0.668, 0, u) }) end end function AIIntro:draw_flash(key, x, y, f, at) local t = f - at if t < 0 or t > 19 then return end self:board(key, x, y, { fade = t < 3 and 1 or lerp(0.941, 0, (t - 3) / 16) }) end function AIIntro:draw_flying_note(f, at) local t = f - at if t < 0 or t >= 40 then return end local x, y if t <= 28 then local u = t / 28 x, y = lerp(-136, 326, u), lerp(-62, -271, u) elseif t <= 30 then local u = clamp01(t - 29) x, y = lerp(344.5, 363, u), -270.5 else x, y = 363, -270.5 end self:board("note_don", x, y + 55.5, { fade = 0.398 }) end function AIIntro:draw_page_1(f) self:board("panel", 0, 0) self:board(f < 74 and "lane_even" or (f < 135 and "lane_ahead_1" or "lane_ahead_2"), 0, 0) self:draw_duel(f < 74 and 2 or (f < 135 and 81 or 242), -92.65, f) self:draw_flying_note(f, 56) self:draw_flying_note(f, 116) if f >= 56 and f <= 96 then self:draw_burst("burst_gold", -137, -6.85, f - 56, 1) self:draw_hit_text("text_good_big", -137, -6.85, f - 56, true) self:draw_burst("burst_gray", -137, 98.65, f - 56, 0.301) self:draw_hit_text("text_ok", -137, 98.65, f - 56, false, true) elseif f >= 116 then self:draw_burst("burst_gold", -137, -6.85, f - 116, 1) self:draw_hit_text("text_good_big", -137, -6.85, math.min(f - 116, 34), true) self:draw_hit_text("text_bad_small", -137, 98.65, math.min(f - 116, 34), false) end self:draw_flash("spirit_gold", -136.7, -6.45, f, 116) self:draw_flash("spirit_gold", -136.7, -6.45, f, 56) self:draw_flash("spirit_silver", -136.7, 98.5, f, 56) self:draw_note(-6.45, f, 0, 51) self:draw_note(99.1, f, 0, 51) self:draw_note(-6.45, f, 60, 113) self:draw_note(99.1, f, 60, 113) if f >= 53 then self:board("bubble_good", -253, -142) end if f >= 156 then local t = f - 156 local s, a if t < 4 then s, a = lerp(2, 1.25, clamp01(t / 3)), lerp(0.5, 0.875, clamp01(t / 3)) elseif t < 9 then s, a = lerp(1, 1.293, (t - 4) / 4), 1 else s, a = lerp(1.22, 1, clamp01((t - 9) / 3)), 1 end self:board("text_ahead", 153, 82, { s = s, fade = a }) end end function AIIntro:draw_page_2(f) self:board("panel", 0, 0) self:board(f < 74 and "lane_even" or (f < 135 and "lane_behind_1" or "lane_behind_2"), 0, 0) self:draw_duel(f < 74 and 4 or (f < 135 and -73 or -238), -92, f) if f >= 56 and f <= 96 then self:draw_hit_text("text_bad_big", -137, -6.85, f - 56, true) self:draw_burst("burst_gray", -137, 98.65, f - 56, 0.301) self:draw_hit_text("text_ok", -137, 98.65, f - 56, false, true) elseif f >= 116 then self:draw_hit_text("text_bad_big", -137, -6.85, math.min(f - 116, 34), true) self:draw_burst("burst_gold", -137, 98.65, f - 116, 1) self:draw_hit_text("text_good_small", -137, 98.65, math.min(f - 116, 34), false) end self:draw_flash("spirit_silver", -136.7, -6.45, f, 56) self:draw_flash("spirit_gold", -136.7, 98.5, f, 116) self:draw_flash("spirit_gold", -136.7, 98.5, f, 56) self:draw_note_label(42.35, f, 0, 53) self:draw_note_label(147.9, f, 0, 53) self:draw_note_label(42.35, f, 60, 113) self:draw_note_label(147.9, f, 60, 113) self:draw_note(-6.45, f, 0, 53) self:draw_note(99.1, f, 0, 53) self:draw_note(-6.45, f, 60, 113) self:draw_note(99.1, f, 60, 113) if f >= 53 then self:board("bubble_miss", -243, -142) end if f >= 170 then self:board("text_behind", 180, 58, { fade = ramp(f, 170, 13) }) end end function AIIntro:draw_blink(key, x, y, f, at, on, cycle) if f < at then return end local g = math.floor(f - at) % cycle if g < on or g == cycle - 1 then self:board(key, x, y) end end function AIIntro:draw_page_3(f) self:board("panel_sections", 0, 0) self:board("lane_section", 0, 61.05) self:draw_blink("frame_section", -182, -23, f, 150, 20, 40) self:board("duel_small", 0, -31.1) self:board("text_section_reward", 0, -188) if f >= 79 then self:board("bubble_sections", -61, -149) end if f >= 184 then self:board("text_more_wins", -242, 185) end if f >= 264 then local t = f - 264 local s, a = 1, 1 if t < 6 then s, a = lerp(3, 1.333, t / 5), lerp(0, 0.832, t / 5) elseif t < 15 then local u = (t - 6) % 4 / 2 s = u <= 1 and lerp(1, 1.03, u) or lerp(1.03, 1, u - 1) end self:board("banner_battle_win", 136, 173, { s = s, fade = a }) end end function AIIntro:draw_page_5(f) self:board("panel", 0, 0) self:board("text_rank_means", 0, -188) self:board("shot_song_select", 206, 71) self:draw_blink("frame_section", 82, 2, f, 39, 20, 40) if f >= 88 then self:board("rank_panel", -157, 14) end end function AIIntro:draw_page_6(f) self:board("panel", 0, 0) self:board("text_rank_needs", 0, -188) self:board("shot_song_select", 189, -10) self:draw_blink("frame_rank", 108, -132, f, 39, 19, 40) if f >= 149 then self:board("text_in_addition", 0, 208) self:board("text_needed", 0, 208) end if f >= 181 then local u = clamp01((f - 181) / 4) local s = lerp(1.429, 1, u) self:board("badge_soul_clear", lerp(56, 0, u), 208, { s = s }) end if f >= 205 then local u = clamp01((f - 205) / 4) local s = lerp(1.429, 1, u) self:board("badge_battle_win", lerp(-40.5, 0, u), 208, { s = s }) end if f >= 100 then self:board("bubble_difficulty", -98, -17) end end function AIIntro:draw_page(f, t) local dot_x = { [0] = -63, -42, -21, 0, 21, 42, 63 } self:board("page_dot", dot_x[self.page], 282) if self.page == 0 or self.page == 4 then self:board("title_glow", 0, 0) local shift_x = self.page == 0 and 4 or 0 self:draw_sparkles(shift_x, self.page == 0 and 0.5 or 0, t) self:board(self.page == 0 and "title_battle" or "title_rank", 0, 0) else self["draw_page_" .. self.page](self, f) end end function AIIntro:draw_tutorial_chara(t) self:put("shadow_makina", 1164.3, 704, { fade = 0.199 }) self:put("shadow_aidon", 142, 705, { fade = 0.199 }) local d, y = track(AIDON_BIG_JUMP, t % 250) self:put("aidon_big", 143.35, 720 - 128 * d + y, { sy = d }) local m = t % 500 local frame, x = 0, 0 if m >= 499 then frame, x = 4, 3 elseif m >= 274 then frame, x = 3, 3 elseif m >= 184 then frame, x = 2, 3 elseif (m >= 66 and m <= 72) or (m >= 127 and m <= 133) then frame = 1 end self:put("makina_teacher", 1132.3 + x, 489.9, { frame = frame }) end function AIIntro:draw_arrows(t) local a = t % 60 local dx, fade if a < 30 then dx, fade = 29.65 * a / 29, 1 else local u = (a - 30) / 29 dx, fade = lerp(30, 40, u), lerp(0.965, 0, u) end if self.page > 0 then self:put("page_arrow", 96 - dx, 250, { fade = fade }) end if self.page < LAST_PAGE then self:put("page_arrow", 1182.9 + dx, 250, { fade = fade, mirror = "horizontal" }) end end function AIIntro:draw_tutorial() local t = self:frame_since(self.tutorial_t0) local f = self:page_frame() self:put("board", OX, OY) self:draw_page(f, t) self:draw_tutorial_chara(t) if t >= INPUT_WAIT then self:draw_arrows(t) end if self.page == 0 then self:draw_rim_hint(898, 652, t, "right") elseif self.page == LAST_PAGE then self:draw_rim_hint(639, 648, t, "left") self:draw_decide_hint(898, 646, t) else self:draw_rim_hint(898, 652, t, "both") end end function AIIntro:draw() self.draw_background() if self.mode == "story" then self:draw_story() return end self:draw_tutorial() if self.mode == "out" then local a = clamp01(self:frame_since(self.out_t0) / OUT_FRAMES) tex.draw_rect(0, 0, tex.get_screen_width(), tex.get_screen_height(), 0, 0, 0, math.floor(255 * a)) end end