entry board titles/comments (entry_* / entry_*_comment_N), costume-menu guide words (guide_select_label / guide_decide_label), the sort window (sort_window_* / folder_sort_select / course_name_*), the dan HUD plate (dan_gauge_name / dan_exam_text_gauge), the N曲 song limit (song_num_max) and the 演奏スキップON badge (skip_badge_text) are read through the engine's tex.skin_text; the only Japanese left in Lua are folder-name matchers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
320 lines
17 KiB
Lua
320 lines
17 KiB
Lua
-- YataiDON-HSS — the song-loading (rainbow) transition, ROUND 11.
|
|
--
|
|
-- The engine (`src/objects/game/transition.cpp`) offers a scripted backdrop: a
|
|
-- skin that defines `SongTransition` gets `update` / `draw_bg` / `draw_info`
|
|
-- called instead of `Transition::draw_default()`. We take `update` + `draw_bg`
|
|
-- only and leave `draw_info` to the engine, so the song title / subtitle stay
|
|
-- the engine's own `OutlinedText` at their `skin_config` positions with the
|
|
-- engine's own fades — the band under them is ours, drawn in `draw_bg`, which
|
|
-- the engine calls first, so the z-order comes out right for free.
|
|
--
|
|
-- ROUND 12 (text alignment). Those `skin_config` positions are now derived from the
|
|
-- movie's own `DefineEditText` records through the arcade pen rule, not eyeballed:
|
|
-- song_title_text ET10 box (0,311)-(1920,439) size 64 border 7 align 2 jp_64
|
|
-- song_sub_title_text ET12 box (0,396.99)-(1920,525.01) size 40 border 0 align 2 jp_64
|
|
-- pen.y = box.top + (box.h - lineHeight)/2 + border, lineHeight = 92 * size/64
|
|
-- -> title cell top 336, subtitle cell top 432.25; the kanji ink inside a jp_64 cell runs
|
|
-- 14..72 of 97, so the cabinet's ink centres are **379.0** and **459.1**. Our
|
|
-- OutlinedText centres its whole image and its kanji ink sits 47.9/1024 * size above
|
|
-- that centre, so `transition_title` y = 379 + 3.0 + 1224 = 1606 and
|
|
-- `transition_subtitle` y = 459.1 + 2.5 + 1224 = 1686 (the +1224 is `rainbow_up`,
|
|
-- which `Transition::draw_song_info` subtracts). Measured in the framebuffer
|
|
-- afterwards: title ink centre 379.0, subtitle 459.5.
|
|
-- See Graphics/loading/MAPPING.md ROUND 12.
|
|
--
|
|
-- WHAT THE ARCADE ACTUALLY DOES (the reference build `loading/loading_song.nulm`, root MC 58,
|
|
-- 110 frames @ 60 fps, labels init 0 / in 5 / stop 55 / out 60 / end 109; the
|
|
-- numbers are exported by scratchpad/anim_export.py into `anim/loading_song.lua`):
|
|
--
|
|
-- depth 0/1 char 47 / 49 two 960x1080 CURVED half-rainbows. f5..f34 they
|
|
-- slide in from off-screen (tx -240 -> 480 / 2160 -> 1440) while
|
|
-- stretching horizontally (sx 0.5 -> 1.0). At f34 they have met
|
|
-- but still leave a wedge of the screen behind them uncovered.
|
|
-- f79..f109 is the exact reverse — they retract sideways.
|
|
-- depth 2 char 51 the flat 7-stripe full-screen rainbow (7x 1920x160
|
|
-- slabs placed at (960,1000)). Present f35..f78 ONLY: it replaces
|
|
-- the two curtains the frame after they meet, and that is what
|
|
-- fills the wedge. Static while present.
|
|
-- depth 3 char 53 an 8x160 gradient drawn 1920x512 at y 568..1080,
|
|
-- **blend = add**, alpha 0 -> 77/256 over f35..f55, held to f60,
|
|
-- back to 0 by f69. A warm glow along the bottom, NOT a dark
|
|
-- strip (round 2 read it as one; round 4 deleted it because the
|
|
-- arcade capture showed nothing dark — both wrong, it is additive).
|
|
-- depth 4 sprite 3 ELEVEN copies of one 128x128 star (char 1) at fixed
|
|
-- positions and scales. No motion, no sprite sheet: the whole
|
|
-- group fades 0 -> 1 over f45..f55 and 1 -> 0 over f60..f69.
|
|
-- depth 5/6 don / katsu 560x560. They fly in along an ARC from off-screen
|
|
-- low (f35 (-166,738)/(2086,738) -> f55 (160,786)/(1760,786), the
|
|
-- ty rising to 656 at f43), hold, dip 786 -> 806 over f60..f65,
|
|
-- then fly straight up to y 294 and fade out by f79.
|
|
-- depth 7 char 56 the 1600x256 title band at (960,400). f45..f55 it
|
|
-- OPENS: sx 0.25 -> 1 with alpha 0 -> 1. f60..f69 it SQUASHES
|
|
-- shut: sx 1 -> 1.5, sy 1 -> 0.1, alpha -> 0.
|
|
-- depth 8+ song title / subtitle / ruby / tips: alpha 0 until f55, 1 at
|
|
-- `stop`, 0 at `out`. The cabinet parks on f55 for the whole load.
|
|
--
|
|
-- MAPPING ONTO THIS ENGINE. There is no "park" here: `song_select.cpp` shows
|
|
-- the transition for exactly `song_info_fade` (delay 266 + duration 266 = 532 ms,
|
|
-- global animation.json ids 0/3) and then `game.cpp` builds a second one
|
|
-- (`is_second`) that runs another 532 ms while the chart loads. So:
|
|
-- scene 1 (is_second == false): t 0..532 ms -> arcade frame 5 .. 55
|
|
-- scene 2 (is_second == true): t 0..532 ms -> arcade frame 60 ..109
|
|
-- Both joins are frames that fully cover the screen (f55 and f60 are both inside
|
|
-- the full-rainbow window), so neither scene change can flash.
|
|
--
|
|
-- The title band and the hint block are the exception: they are driven off the
|
|
-- engine's own fade curves rather than off the arcade frame, because they have
|
|
-- to appear and disappear in lockstep with the engine-drawn title. The arcade's
|
|
-- band open is linear in both sx and alpha simultaneously, so re-timing it onto
|
|
-- `song_info_fade` keeps its shape exactly and only changes its window.
|
|
--
|
|
-- TEXTURE SLOTS. `build/generated/texture_ids_generated.h` gives this subset
|
|
-- seven names and nothing else loads, so the new layers are packed into the two
|
|
-- that `draw_default()` used for 8x8 blanks and that nothing reads once a skin
|
|
-- scripts `SongTransition`. See Graphics/loading/MAPPING.md ROUND 11.
|
|
-- rainbow_bg the flat rainbow, 1:1 with the screen
|
|
-- rainbow_bg_top atlas: curtain L (0,0,960,1080) | curtain R (960,0,960,1080)
|
|
-- rainbow_bg_bottom atlas: band (0,0,1600,256) / glow (0,288,1600,512)
|
|
-- / sparkle (0,832,128,128)
|
|
-- text_bg 8x8 transparent — suppresses the engine's own band draw
|
|
|
|
local _ok, anim = pcall(require, "anim/sampler")
|
|
local CLIP = (_ok and anim and anim.safe) and anim.safe("anim/loading_song") or nil
|
|
|
|
SongTransition = {}
|
|
SongTransition.__index = SongTransition
|
|
|
|
local FADE_DELAY, FADE_MS, FADE_OUT_MS = 266.0, 266.0, 133.0
|
|
local SCENE_MS = FADE_DELAY + FADE_MS -- 532 ms: how long each scene shows
|
|
|
|
local F_IN, F_STOP, F_OUT, F_END = 5, 55, 60, 109
|
|
local F_FULL_A, F_FULL_B = 35, 78 -- the full-rainbow window
|
|
|
|
-- sub-rects inside the two atlases
|
|
local SRC_CURT_L = { x = 0, y = 0, width = 960, height = 1080 }
|
|
local SRC_CURT_R = { x = 960, y = 0, width = 960, height = 1080 }
|
|
local SRC_BAND = { x = 0, y = 0, width = 1600, height = 256 }
|
|
local SRC_GLOW = { x = 0, y = 288, width = 1600, height = 512 }
|
|
local SRC_STAR = { x = 0, y = 832, width = 128, height = 128 }
|
|
|
|
-- rest positions written into texture.json; every draw below passes a delta off
|
|
-- them. The two tables must agree (scratchpad/r11ld/bake_r11.py writes the json).
|
|
local REST_CHARA_L = { -120, 506 }
|
|
local REST_CHARA_R = { 1480, 506 }
|
|
local REST_HINT = { 0, 568 }
|
|
|
|
-- sprite 3's eleven star placements, world coords (the sprite sits at (960,540)
|
|
-- with an identity matrix, so its children's tx/ty already ARE screen pixels).
|
|
local STARS = {
|
|
{ 209.75, 542.55, 0.59998 }, { 92.35, 283.00, 0.47829 },
|
|
{ 154.20, 330.75, 0.26784 }, { 292.35, 246.35, 0.44998 },
|
|
{ 1259.00, 201.85, 0.78998 }, { 1845.70, 228.45, 0.37422 },
|
|
{ 1791.70, 268.40, 0.24998 }, { 1388.50, 664.80, 0.44713 },
|
|
{ 1821.20, 423.20, 0.56000 }, { 1472.00, 607.70, 0.25038 },
|
|
{ 552.60, 628.95, 0.74998 },
|
|
}
|
|
|
|
local function clamp(v, a, b) if v < a then return a elseif v > b then return b else return v end end
|
|
|
|
-- Fallbacks for every value the generated table supplies, so a missing or
|
|
-- renamed `anim/loading_song.lua` degrades to the arcade's own end states
|
|
-- instead of taking the loading screen down.
|
|
local function track(name, f, field, default)
|
|
if not CLIP then return default end
|
|
local v = CLIP:get(name, f, field)
|
|
if v == nil then return default end
|
|
return v
|
|
end
|
|
|
|
-- 演奏スキップON badge. `loading/loading_song.nulm`: mc `enso_skip_on` (sprite 46)
|
|
-- sits at stage (350, 106) and holds shape 43, whose local bounds are
|
|
-- (-312,-52)..(312,52) -- a 624x104 pill, so its top-left is (38, 54). Its child
|
|
-- `text_enso_skip` is a DefineEditText with box (-2,-2)..(598,78) placed at
|
|
-- (52, 71), i.e. stage x 50..650 / y 69..149, centre-aligned at font size 30.
|
|
local SKIP_BADGE_X, SKIP_BADGE_Y = 38, 54
|
|
local SKIP_BADGE_CX = 350 -- 38 + 624/2, the EditText's centre
|
|
local SKIP_TEXT_CY = 109 -- 69 + 80/2, the EditText box's centre
|
|
local SKIP_TEXT_SIZE = 30
|
|
-- `put()` adds the texture's OWN texture.json origin to the coordinates it is
|
|
-- given, so every caller passes that origin as `rest` to cancel it out (see
|
|
-- REST_HINT). badge.png declares x=38,y=54 -- the arcade position -- so its rest
|
|
-- is the same pair; passing ORIGIN instead drew it at (76,108).
|
|
local SKIP_REST = { 38, 54 }
|
|
|
|
function SongTransition.new(title, subtitle, is_second)
|
|
local self = setmetatable({}, SongTransition)
|
|
self.is_second = is_second and true or false
|
|
self.t0 = nil
|
|
self.f = self.is_second and F_OUT or F_IN
|
|
self.info = 0 -- band / hint opacity, 0..1
|
|
self.rainbow = tex.load_texture("global/rainbow_transition/rainbow_bg")
|
|
self.atlas_c = tex.load_texture("global/rainbow_transition/rainbow_bg_top")
|
|
self.atlas_b = tex.load_texture("global/rainbow_transition/rainbow_bg_bottom")
|
|
self.chara_l = tex.load_texture("global/rainbow_transition/chara_left")
|
|
self.chara_r = tex.load_texture("global/rainbow_transition/chara_right")
|
|
self.hint = tex.load_texture("global/rainbow_transition/chara_center")
|
|
-- The hint block is baked in Japanese. Any other interface language draws the
|
|
-- three lines as text (skin_config transition_hint_title / _line1 / _line2) at the
|
|
-- rows measured off the texture, on the same fade.
|
|
local lang = tex.language and tex.language() or "ja"
|
|
if lang ~= "ja" and text and text.create_text then
|
|
local white, black = { 255, 255, 255, 255 }, { 0, 0, 0, 255 }
|
|
self.hint_title = text.create_text("transition_hint_title", white, black, false, 4.0, 2.0)
|
|
self.hint_line1 = text.create_text("transition_hint_line1", white, black, false, 3.0, 2.0)
|
|
self.hint_line2 = text.create_text("transition_hint_line2", white, black, false, 3.0, 2.0)
|
|
end
|
|
-- 演奏スキップON badge (the reference build loading/loading_song.nulm, mc `enso_skip_on`)
|
|
self.skip_badge = tex.load_texture("loading/skip_badge/badge")
|
|
-- NOT latched here: GameScreen::on_screen_start builds the transition
|
|
-- (game.cpp:69) BEFORE it calls init_skip() (game.cpp:76), so at construction
|
|
-- time tex.skip_enabled() is still reporting the PREVIOUS enso. Resolved on
|
|
-- first draw instead -- see SongTransition:skip_armed().
|
|
self.skip_on = nil
|
|
if text and text.create_raw_text then
|
|
-- The cabinet does NOT bake this label: `text_enso_skip` is a Lumen
|
|
-- DefineEditText whose HTML is
|
|
-- <p align="center"><font face="MS Pゴシック" size="30"
|
|
-- color="#000000">演奏スキップON</font></p>
|
|
-- black, size 30, centred, NO outline (its border field is 0.0). It has
|
|
-- no language variants, so the cabinet shows the Japanese string in every
|
|
-- locale and so do we. Built HERE and not in skip_armed(): the glyph
|
|
-- atlas needs a frame to upload, so creating it on the first drawn frame
|
|
-- left the first frames showing an empty pill.
|
|
self.skip_text = text.create_raw_text(tex.skin_text("skip_badge_text"), SKIP_TEXT_SIZE,
|
|
{ 0, 0, 0, 255 }, { 0, 0, 0, 0 }, false, 0, 2)
|
|
end
|
|
return self
|
|
end
|
|
|
|
-- Resolved lazily, once, on the first frame that actually draws: by then
|
|
-- init_skip() has run and tex.skip_enabled() is true iff the feature ARMED for
|
|
-- this enso (skin row on + a player has it on + a free drum, so false in 2P).
|
|
function SongTransition:skip_armed()
|
|
if self.skip_on == nil then
|
|
self.skip_on = (tex.skip_enabled ~= nil) and tex.skip_enabled() or false
|
|
end
|
|
return self.skip_on
|
|
end
|
|
|
|
function SongTransition:update(current_ms, rainbow_up, song_info_fade)
|
|
if not self.t0 then self.t0 = current_ms end
|
|
local t = clamp(current_ms - self.t0, 0, SCENE_MS)
|
|
if self.is_second then
|
|
self.f = F_OUT + (F_END - F_OUT) * (t / SCENE_MS)
|
|
self.info = clamp(1.0 - t / FADE_OUT_MS, 0, 1)
|
|
else
|
|
self.f = F_IN + (F_STOP - F_IN) * (t / SCENE_MS)
|
|
-- the engine hands us `song_info_fade`; use it so the band can never
|
|
-- drift from the title the engine draws on top of it
|
|
self.info = clamp(song_info_fade or ((t - FADE_DELAY) / FADE_MS), 0, 1)
|
|
end
|
|
end
|
|
|
|
-- Draw one texture into an absolute screen rect. `scale = 0` makes the engine's
|
|
-- `x2[i] * scale + params.x2` collapse to exactly `params.x2`, which is the only
|
|
-- way to get independent width and height out of `draw_texture`.
|
|
local function put(id, rest, x, y, w, h, a, src, blend)
|
|
if not id then return end
|
|
tex.draw_texture(id, {
|
|
x = x - rest[1], y = y - rest[2],
|
|
scale = 0, x2 = w, y2 = h,
|
|
fade = a, src = src, blend = blend,
|
|
})
|
|
end
|
|
|
|
local ORIGIN = { 0, 0 }
|
|
|
|
|
|
function SongTransition:draw_bg(total_offset, rainbow_up, mini_up, chara_down)
|
|
local f = self.f
|
|
|
|
-- 1. backdrop: two curved curtains, or the flat full-screen rainbow
|
|
if f >= F_FULL_A and f <= F_FULL_B then
|
|
put(self.rainbow, ORIGIN, 0, 0, 1920, 1080, 1)
|
|
else
|
|
local lx = track("#47@0", f, "tx", -240)
|
|
local ls = track("#47@0", f, "sx", 0.5)
|
|
local rx = track("#49@1", f, "tx", 2160)
|
|
local rs = track("#49@1", f, "sx", 0.5)
|
|
put(self.atlas_c, ORIGIN, lx - 480 * ls, 0, 960 * ls, 1080, 1, SRC_CURT_L)
|
|
put(self.atlas_c, ORIGIN, rx - 480 * rs, 0, 960 * rs, 1080, 1, SRC_CURT_R)
|
|
end
|
|
|
|
-- 2. additive glow along the bottom (char 53, 1920x512 at y568, alpha -> 77/256)
|
|
local ga = track("#55@3", f, "a", 0)
|
|
if ga > 0.002 then
|
|
put(self.atlas_b, ORIGIN, 0, 568, 1920, 512, ga, SRC_GLOW, "additive")
|
|
end
|
|
|
|
-- 3. the eleven sparkles — fixed placements, one group alpha
|
|
local sa = track("#3@4", f, "a", 0)
|
|
if sa > 0.002 then
|
|
for i = 1, #STARS do
|
|
local s = STARS[i]
|
|
local d = 128 * s[3]
|
|
put(self.atlas_b, ORIGIN, s[1] - d * 0.5, s[2] - d * 0.5, d, d, sa, SRC_STAR)
|
|
end
|
|
end
|
|
|
|
-- 4. don / katsu on their arc (560x560, registration = canvas centre)
|
|
local da = track("don", f, "a", 1)
|
|
if da > 0.002 then
|
|
put(self.chara_l, REST_CHARA_L,
|
|
track("don", f, "tx", 160) - 280, track("don", f, "ty", 786) - 280,
|
|
560, 560, da)
|
|
end
|
|
local ka = track("katsu", f, "a", 1)
|
|
if ka > 0.002 then
|
|
put(self.chara_r, REST_CHARA_R,
|
|
track("katsu", f, "tx", 1760) - 280, track("katsu", f, "ty", 786) - 280,
|
|
560, 560, ka)
|
|
end
|
|
|
|
-- 5. the title band. Arcade open: sx 0.25 -> 1 and alpha 0 -> 1, both linear
|
|
-- and simultaneous, re-timed onto the engine's own title fade. Arcade close:
|
|
-- sx 1 -> 1.5, sy 1 -> 0.1, alpha -> 0, on the fade-out.
|
|
local info = self.info
|
|
if info > 0.002 then
|
|
local bw, bh
|
|
if self.is_second then
|
|
bw = 1600 * (1.5 - 0.5 * info)
|
|
bh = 256 * (0.1 + 0.9 * info)
|
|
else
|
|
bw = 1600 * (0.25 + 0.75 * info)
|
|
bh = 256
|
|
end
|
|
put(self.atlas_b, ORIGIN, 960 - bw * 0.5, 400 - bh * 0.5, bw, bh, info, SRC_BAND)
|
|
-- 6. the hint block (「ゲームのヒント」 + 3 lines) on the same curve: the
|
|
-- arcade turns every text field on at `stop` and off at `out`.
|
|
if self.hint_title then
|
|
-- texture rows (alpha > 200, outline included): title 165..202, line 1 228..257,
|
|
-- line 2 282..310, all centred on x = 960; the texture sits at y = 568.
|
|
local function centred(t, y)
|
|
if t then t:draw({ x = math.floor(960 - t.width / 2 + 0.5), y = y, fade = info }) end
|
|
end
|
|
centred(self.hint_title, 568 + 165)
|
|
centred(self.hint_line1, 568 + 228)
|
|
centred(self.hint_line2, 568 + 282)
|
|
else
|
|
put(self.hint, REST_HINT, 0, 568, 1920, 512, info)
|
|
end
|
|
end
|
|
|
|
-- 7. 演奏スキップON badge, top-left, on the same fade as the band. The arcade
|
|
-- shows it ONLY on the song-loading screens (loading/loading_song,
|
|
-- enso_ai/loading/loading_ai and the two collab AI loadings) -- there is no
|
|
-- `enso_skip_on` anywhere in the song-select lumens, so nothing is drawn
|
|
-- there. It is skipped in 2P for free: tex.skip_enabled() is false whenever
|
|
-- the feature did not arm, which is exactly the arcade's greyed-row case.
|
|
if self:skip_armed() and self.skip_badge then
|
|
put(self.skip_badge, SKIP_REST, SKIP_BADGE_X, SKIP_BADGE_Y, 624, 104, info)
|
|
if self.skip_text then
|
|
self.skip_text:draw({
|
|
x = SKIP_BADGE_CX - self.skip_text.width / 2,
|
|
y = SKIP_TEXT_CY - self.skip_text.height / 2,
|
|
fade = info,
|
|
})
|
|
end
|
|
end
|
|
end
|