Files
YataiDONNijiiro/Scripts/song_select/dan_doors.lua
T
2026-09-02 14:25:04 -04:00

530 lines
26 KiB
Lua

-- YataiDON-HSS — the 段位道場 shutter (doors + scrim + kanji stamp).
--
-- ROUND 23 RECONSTRUCTION. This file and its Graphics tree (`Graphics/**/dan_doors*`)
-- were confirmed missing from disk by ROUND 22 (see Graphics/dan_select/MAPPING.md
-- ROUND 22) — gone since at least a round-19-era scratchpad snapshot, silently, because
-- both call sites `pcall`-guard the `require`. This round rebuilt it from three sources
-- ONLY, per the standing "never invent data" rule:
-- 1. Graphics/dan_select/MAPPING.md ROUND 11 / 14 / 17 — every frame number, every
-- constant (CLOSE_FROM/TO, STAMP_FROM/TO, OPEN_FROM/TO, the kanji cell centres,
-- the se_daniodai_v12e/intro -> dan_intro sound history) is documented there.
-- 2. scratchpad/r17dn/patch_doors.py and patch_doors2.py — the FINAL, tested ROUND 17
-- function bodies (draw_frame/draw/draw_open/phases/frame_of/set_progress),
-- recovered verbatim (they were python string-patches against this exact file, so
-- their "old"/"new" blocks ARE the shipped Lua source at that point in history).
-- 3. Scripts/anim/dani_intro.lua — the exported the reference build track data itself, which was
-- NEVER deleted (only this consuming module was). Its rows for #1@3 (scrim),
-- #5@1/#5@2 (right/left door), #7@4/#9@5/#11@6/#13@7 (段/位/道/場) match the
-- MAPPING doc's frame numbers exactly, confirming the reconstruction.
--
-- What could NOT be recovered from those three sources and was therefore NOT invented:
-- nothing load-bearing turned out to be missing. `Graphics/song_select/dan_transition/
-- background.png` — the 2000x1080 atlas ROUND 11's "What ships" table describes
-- (door panel (0,0,1200,1080); kanji cells (1200,0)/(1600,0)/(1200,400)/(1600,400),
-- 400x400 each; scrim patch (1200,800,64,64)) — turned out to still be on disk, verified
-- this round by cropping all six regions and confirming non-transparent content in each
-- (see ROUND 23 section below for the exact bboxes measured). Only the *Lua module*
-- that reads it was gone, not the art. `Sounds/song_select/dan_intro.ogg` was also
-- still present, and `Graphics/song_select/animation.json` id 38's `duration` (2300 ms)
-- and comment ("Scripts/song_select/dan_doors.lua draws both halves") had ALSO survived
-- untouched — this file is the only piece that needed rebuilding.
local anim = require("anim/sampler")
local CLIP = anim.safe("anim/dani_intro")
local M = {}
-- Stage geometry, from the MAPPING doc's "arcade rig, decoded" table (ROUND 11).
local SCREEN_W, SCREEN_H = 1920, 1080
local DOOR_W = 1200 -- door panel local width (main__shape5)
local KANJI = 400 -- each kanji cell is 400x400 (main__shape7/9/11/13)
-- background.png atlas layout, ROUND 11 "What ships":
-- (0,0,1200,1080) door panel | (1200,0)/(1600,0)/(1200,400)/(1600,400) the four
-- kanji, 400x400 each | (1200,800,64,64) an opaque black patch for the scrim.
local SCRIM_SRC = { x = 1200, y = 800, width = 64, height = 64 }
-- kanji cell -> its arcade track name, in reading order 段位道場 (ROUND 11 table:
-- #7@4=段 centre(455,255), #9@5=位 centre(1420,255), #11@6=道 centre(455,765),
-- #13@7=場 centre(1420,765) — the (i<=2)/(i odd) fallback centres below mirror that).
local CELLS = {
{ track = "#7@4", sx = 1200, sy = 0 }, -- 段
{ track = "#9@5", sx = 1600, sy = 0 }, -- 位
{ track = "#11@6", sx = 1200, sy = 400 }, -- 道
{ track = "#13@7", sx = 1600, sy = 400 }, -- 場
}
-- Frame map.
--
-- ROUND 64 (r64-danselect-fidelity) RE-DERIVATION. The report was 「段位道場的
-- entry動畫有誤,重新查」, and re-reading the cabinet's own script found two
-- real errors in ROUND 14/17's map. The export itself is NOT at fault (the
-- ROUND 61 trap): `anim/dani_intro.lua` was dumped `--range 0,262 --all` and
-- `lumen_anim_dump loading_dani_intro.nulm --list` reports sprite 15 with
-- exactly 263 frames and labels init@0 / in@5 / stop@248 / out@253 / end@262,
-- so every leg below is covered by real data.
--
-- WHO PLAYS IT, and WHERE. `script_lua/dani_select/dani_select_introduction_
-- main.lua` loads `lumen/000_default/enso_dani/loading_dani/loading_dani_intro`
-- as `introduction_instance` -- root depth 16 of dani_select.nulm, i.e. the
-- TOP-most layer of the DAN_SELECT scene -- and `dani_select_all.lua`'s state
-- machine plays it from the `IntroductionMain` state, which runs AFTER
-- Begin/Loading/SetUp/StartWait, i.e. after the whole screen has loaded:
-- PlayIntroduction : TSound:Play("se_daniodai_v12e","intro")
-- mc_main.main:GotoAndPlay("in") -- frame 5
-- Is_EndAnimation : if not IsPlay() then GotoAndPlay("out") end
-- -> DaniSelectMain: timer_:StartCount(); name_plate_:SetVisible(true)
-- So on the cabinet the ENTIRE 4.2 s runs on the dan-select screen and nothing
-- of it runs on song select. Our engine has to cover its own (blocking,
-- multi-second) DAN_SELECT load, which the cabinet does in a state with
-- nothing on screen, so the song-select half stays -- but it must stop
-- inventing motion the cabinet does not have.
--
-- ERROR 1 (fixed here): the close leg played the DOOR track BACKWARDS,
-- f200 -> f182, so our shutter slid in from off-screen. There is no such move
-- anywhere in the clip. `#5@1`/`#5@2` are PLACED at f35 already shut (tx 960,
-- sx +-1) and do not move again until f187. What actually happens over f0..62
-- is the SCRIM: `#1@3` is alpha 1 (a full-screen black) from f0 to f55 and
-- eases to 0.5 by f62 -- the screen is simply black, then 50 % black over the
-- shut shutter. The doors never move on the way in. The close leg is therefore
-- now a pure OPACITY ramp over the f62 pose: invented (we are fading out of a
-- live wheel, the cabinet is fading out of nothing), but it no longer
-- fabricates a door slide.
--
-- ERROR 2 (fixed here): the DAN_SELECT half stopped at f203. It is not movie
-- tail past there: `#3@0` -- the dan-dojo BACKGROUND, the same night-torch art
-- as Graphics/dan_select/global/bg.png, verified by rendering
-- `lumen_shape_dump -m -r 15 -f 215` -- pushes in from scale 1.0 to 1.25 over
-- f187..253 and only then cross-fades out over f253..262 (`out`). Cutting at
-- f203 threw away the push-in and the cross-fade, which is the half of the
-- entry animation the player actually reads as "entering the dojo".
local CLOSE_POSE = 62 -- shut doors, scrim already eased to 0.5
local STAMP_FROM = 62 -- the arcade's scrim has just settled to 0.5
local STAMP_TO = 182 -- all four landed, before the fade
local OPEN_FROM = 182 -- kanji fade (182-185), doors part (187-200),
local OPEN_TO = 262 -- scrim clears (203), bg push-in + `out` fade
local MS_PER_FRAME = 1000.0 / 60.0
-- INVENTED, and the only invented number left in the map: the wheel has to go
-- somewhere. 300 ms is ROUND 17's value, kept so Graphics/song_select/
-- animation.json id 38's `duration` (2300.0) still equals CLOSE_MS + STAMP_MS
-- exactly and no timing outside this file moves.
local CLOSE_MS = 300.0
local STAMP_MS = (STAMP_TO - STAMP_FROM) * MS_PER_FRAME -- 120 frames = 2000 ms
local OPEN_MS = (OPEN_TO - OPEN_FROM) * MS_PER_FRAME -- 80 frames = 1333 ms
-- Total ms of the SONG_SELECT half. MUST agree with Graphics/song_select/animation.json
-- id 38 `duration` (2300.0, confirmed still on disk this round: exactly CLOSE_MS+STAMP_MS).
-- The engine hands the real progress in via set_progress() every frame on a build with
-- `SongSelect:draw_top`, so this constant is only load-bearing on an engine WITHOUT it
-- (the self-clocked fallback in M.progress() below).
local DURATION_MS = 2300.0
local function duration_ms() return DURATION_MS end
local FB_SCRIM = 0.5 -- #1@3's settled alpha (f62..193 in the arcade data)
local function clamp01(v) return math.max(0.0, math.min(1.0, v)) end
local function clip() return CLIP end
-- ── the atlas ────────────────────────────────────────────────────────────────
local _id, _id_tried
local function tex_id()
if not _id_tried then
_id_tried = true
_id = tex.load_texture("song_select/dan_transition/background")
end
return _id
end
-- ROUND 64 -- the DAN_SELECT screen's own background, which is also the intro
-- clip's `#3@0`. Resident only while DAN_SELECT is live; nil everywhere else,
-- which is exactly the gate the push-in needs.
local _bg, _bg_tried
local function bg_id()
if not _bg_tried then
_bg_tried = true
_bg = tex.load_texture("dan_select/global/bg")
end
return _bg
end
-- ── fallbacks, reached only if anim/dani_intro.lua is itself missing/malformed
-- (anim.safe hands back the NULL clip) — the closed-form end states the frame map
-- above degrades to, so a renamed/corrupted data table cannot take the screen down.
-- ROUND 64: the shut pose is the ONLY pose the fallback can honestly assert --
-- the doors never travel except over f187..200, whose curve is exactly the data
-- this fallback exists because we lost. Right door (#5@1) tx 960; left door
-- (#5@2) tx 960 with sx -1, i.e. its drawn left edge at 960 - 1200 = -240.
local function fallback_door()
return -240, 960, DOOR_W, 0, FB_SCRIM
end
local function fallback_kanji(i, shown)
if not shown or shown <= 0 then return nil end
return 1.0, 1.0 -- scale 1, alpha 1 — the landed pose, nothing finer is recoverable
end
-- ── sound. ROUND 14b: resident name is the file's STEM, not its skin-relative path
-- (`audio.cpp` prefixes only files inside a screen subfolder); the corrected cue is
-- `Sounds/song_select/dan_intro.ogg`, still present on disk.
local function fire_sfx()
if M.sfx_done then return end
if audio and audio.play_sound then audio.play_sound("dan_intro", "sound") end
M.sfx_done = true
end
-- ROUND 17: two segments only — close then stamp — and the half ENDS parked on
-- STAMP_TO. A `duration` shorter than close+stamp still compresses the two
-- proportionally (that is what keeps the old 1100 ms value producing the round-11
-- look); anything longer parks the leftover on STAMP_TO, which is the cabinet's
-- own hold pose. The shipped 2300 ms leaves none, because our hold is now the
-- real DAN_SELECT load rather than invented dead time.
local function phases()
local d = duration_ms()
local move = CLOSE_MS + STAMP_MS
local k = (d >= move) and 1.0 or (d / move) -- compress only if we must
local c = CLOSE_MS * k
local st = STAMP_MS * k
return c / d, (c + st) / d
end
-- u (0..1 across our transition) -> arcade frame.
-- ROUND 64: the close leg no longer travels; it holds CLOSE_POSE and only the
-- overall opacity moves (see the frame-map comment).
local function frame_of(u)
local close_u, stamp_u = phases()
if u <= close_u then
return CLOSE_POSE
elseif u <= stamp_u then
local v = (u - close_u) / (stamp_u - close_u)
return STAMP_FROM + (STAMP_TO - STAMP_FROM) * v
end
return STAMP_TO -- parked: doors shut, kanji up
end
-- ── the shared frame painter ────────────────────────────────────────────────
-- ROUND 17: both halves paint the SAME arcade frame map, so the painting is one
-- function taking an arcade frame.
-- ROUND 64: the second argument is now an OVERALL OPACITY (nil = 1), not a
-- "closedness". The cabinet's shutter has exactly one pose on the way in -- shut
-- -- so our invented close is a fade of that pose rather than a fabricated
-- slide, and the fade has to reach the doors too, not only the scrim.
local function draw_frame(f, op)
local id = tex_id()
if not id then return false end
local c = clip()
op = (op == nil) and 1.0 or clamp01(op)
if op <= 0 then return true end
-- ROUND 64 -- the background, `#3@0`, arcade depth 0: BELOW both doors and
-- the scrim. It is the dan-dojo night art (= Graphics/dan_select/global/
-- bg.png), placed at f35 and holding scale 1 until f187, then pushing in to
-- 1.25 by f253 and cross-fading out over f253..262. Only the DAN_SELECT
-- half ever sees it (during the stamp the opaque shutter covers it, and on
-- SONG_SELECT the texture is not even resident) -- bg_id() is nil there and
-- the whole block is skipped, so nothing regresses on an engine or screen
-- without it.
local btx, bty, bsx, _, ba = c:at("#3@0", f)
if btx and ba and ba > 0 then
local bid = bg_id()
if bid then
local w, h = SCREEN_W * (bsx or 1), SCREEN_H * (bsx or 1)
tex.draw_texture(bid, {
x = (btx or 960) - w * 0.5, y = (bty or 540) - h * 0.5,
x2 = w - SCREEN_W, y2 = h - SCREEN_H, fade = ba * op,
})
end
end
-- doors -----------------------------------------------------------------
-- A track row is (tx, ty, sx, sy, a) in the arcade's 1920x1080 stage. The
-- panel's local box is x 0..1200, y 0..1080 with its origin at the placement,
-- so screen span = tx .. tx + 1200*sx and top = ty - 540. sx < 0 IS the
-- mirror (the left door is the same shape flipped), which is why the left
-- door's x is tx + 1200*sx, i.e. tx minus the drawn width.
local lx, rx, dw, dtop, scrim
local rtx, rty, rsx = c:at("#5@1", f)
local ltx, _, lsx = c:at("#5@2", f)
if rtx and ltx then
dw = DOOR_W * math.abs(rsx)
rx = rtx
lx = ltx + DOOR_W * lsx
dtop = rty - SCREEN_H * 0.5
scrim = c:get("#1@3", f, "a") or FB_SCRIM
else
lx, rx, dw, dtop, scrim = fallback_door()
end
-- left door = the same panel mirrored (a negative src width is raylib's flip)
tex.draw_texture(id, { src = { x = 0, y = 0, width = -DOOR_W, height = SCREEN_H },
x = lx, y = dtop, x2 = dw, y2 = SCREEN_H, fade = op })
tex.draw_texture(id, { src = { x = 0, y = 0, width = DOOR_W, height = SCREEN_H },
x = rx, y = dtop, x2 = dw, y2 = SCREEN_H, fade = op })
-- the full-screen black at depth 3: above both doors, below all four kanji
if scrim and scrim > 0 then
tex.draw_texture(id, { src = SCRIM_SRC,
x = 0, y = 0, x2 = SCREEN_W, y2 = SCREEN_H,
fade = scrim * op })
end
-- kanji ------------------------------------------------------------------
for i = 1, #CELLS do
local cell = CELLS[i]
local cx, cy, s, a
local tx, ty, sx, _, al = c:at(cell.track, f)
-- A track's rows start at the frame the glyph is PLACED; the sampler
-- clamps below that, so an explicit gate is needed or 段 would be on
-- screen from frame 0.
local first = c:has(cell.track) and c.tracks[cell.track][1][1] or nil
if tx and first and f >= first then
cx, cy, s, a = tx, ty, sx, al
elseif not tx then
local fs, fa = fallback_kanji(i, (f >= STAMP_TO) and 1 or 0)
if fs then
cx = (i == 1 or i == 3) and 455 or 1420
cy = (i <= 2) and 255 or 765
s, a = fs, fa
end
end
if cx and a and a > 0 then
local w = KANJI * s
tex.draw_texture(id, {
src = { x = cell.sx, y = cell.sy, width = KANJI, height = KANJI },
x = cx - w * 0.5,
y = cy - w * 0.5,
x2 = w, y2 = w, fade = a * op,
})
end
end
return true
end
-- ── arming / progress ───────────────────────────────────────────────────────
-- `M.arm(active)` is called every SONG_SELECT frame from `draw_overlays(state)`
-- with `state == STATE_DAN_SELECTED`; it is the ONLY signal on an engine without
-- `draw_top` (the self-clocked fallback below). `M.set_progress(p)` is called
-- every SONG_SELECT frame from `draw_top` with `DanTransition::progress()` (0..1,
-- or a negative value when no transition is running) on an engine that has it —
-- when present it always wins over the fallback clock.
M.armed = false
M.engine_u = nil
M.has_draw_top = false
M.t0 = nil
M.armed_open = false
M.open_t0 = nil
M.sfx_done = false
function M.arm(active)
M.armed = active and true or false
if not M.armed then
M.t0 = nil -- let the fallback clock restart cleanly next time
end
end
function M.set_progress(p)
M.has_draw_top = true
if p and p >= 0 then
M.engine_u = p
else
M.engine_u = nil
-- ROUND 17 -- TEARDOWN. This is called from SongSelect:draw_top on EVERY
-- song-select frame, with -1 when no transition is running, so it is the one
-- place guaranteed to run when the rig must not be on screen. Clearing the
-- DAN_SELECT half's state here is what stops a stale overlay surviving a
-- back-out of DAN_SELECT (or an aborted transition) and re-appearing over the
-- wheel: whatever half-open pose was left is dropped the first frame the
-- song-select screen is live again.
M.armed_open = false
M.open_t0 = nil
end
end
function M.progress()
if M.has_draw_top then
-- the engine supplies real progress every frame; nil/negative = not running
if M.engine_u and M.engine_u >= 0 then return clamp01(M.engine_u) end
return nil
end
-- fallback: no draw_top on this engine build, self-clock off arm()
if not M.armed then return nil end
local now = tex.get_current_ms()
if not M.t0 then M.t0 = now end
local d = duration_ms()
local u = (now - M.t0) / d
if u >= 1.0 then return nil end
return u
end
-- ── half 1: SONG_SELECT (close + stamp) ─────────────────────────────────────
-- Returns true when it drew anything.
function M.draw()
local u = M.progress()
if not u then return false end
fire_sfx(u)
local close_u = phases()
local f = frame_of(u)
-- ROUND 64: over the invented close leg the pose is frozen at CLOSE_POSE and
-- only the opacity ramps in; from there on the clip's own alphas rule.
if not draw_frame(f, (u <= close_u) and (u / close_u) or nil) then return false end
-- ROUND 17: arm the DAN_SELECT half. Set on every frame the rig is live, so
-- an abort (the transition never completing) leaves it armed only until the
-- next set_progress(-1), which is the very next song-select frame.
--
-- ROUND 23 FIX: guard the re-arm to first-arm only. `Scripts/global/
-- indicator.lua`'s own fallback slot (`Indicator:draw()`, kept for an engine
-- without draw_top) calls `dan_doors.draw()` on EVERY screen `Indicator` is
-- used on, DAN_SELECT included -- and once `set_progress` has been called at
-- least once, `M.progress()`'s engine-driven branch returns the LAST
-- `engine_u` it was given (SONG_SELECT stops calling `set_progress` the
-- instant the screen changes to DAN_SELECT, so that value is never updated
-- to nil again). The fallback therefore kept calling `M.draw()` -> this
-- branch -> `M.open_t0 = nil` on every single DAN_SELECT frame, which reset
-- `M.draw_open()`'s wall clock before it could ever accumulate past 0 --
-- the doors visibly never opened, verified as stuck at the shut/stamped
-- pose for 6.5+ seconds in `scratchpad/r23dn/open_probe/`. Only re-arming
-- when NOT already armed lets `draw_open()`'s own clock run once started.
if not M.armed_open then
M.armed_open = true
M.open_t0 = nil
end
M._drawn_ms = tex.get_current_ms()
return true
end
-- Pure layering helper for `Scripts/global/indicator.lua`'s fallback slot: true
-- iff `M.draw()` already painted this exact engine frame, so the fallback call
-- (kept only for an engine without the real SONG_SELECT top slot) never draws
-- the rig twice in one frame.
function M.drawn_this_frame()
return M._drawn_ms ~= nil and M._drawn_ms == tex.get_current_ms()
end
-- ── half 2: DAN_SELECT (the reveal) ─────────────────────────────────────────
-- Called from `Indicator:draw_top()`, the top-most slot on DAN_SELECT (added
-- round 17: DanSelectScreen::draw() calls it after the coin overlay, the
-- DAN_SELECT plate and the allnet chip). ROUND 64: plays arcade f182..262 -- the
-- WHOLE remaining clip, not the f182..203 stub -- on a plain wall clock at 1:1
-- with the cabinet, then tears itself down so nothing survives into the screen.
-- Returns true while it is drawing. 80 frames = 1333 ms, which is the window
-- DanSelectScreen::tick_timer waits out before StartCount()ing the credit clock
-- (the cabinet's IntroductionMain -> DaniSelectMain hand-off).
--
-- The clock starts on the FIRST frame this runs rather than at the screen
-- change, which is deliberate: DAN_SELECT's init can block the render loop for
-- a couple of seconds and anchoring to the change would make the doors jump
-- straight to open on the first frame the loop comes back. Starting here means
-- the open plays in full, right after the load.
--
-- ROUND 86 (r86) -- THE SECOND ENTRY PATH, and the defect it caused.
--
-- Reported: 「段位道場的進入動畫消失了」. ROUND 83 promoted 段位道場 to a mode board on
-- the ENTRY screen, so the dojo is now reachable as ENTRY -> DAN_SELECT with no
-- SONG_SELECT in between. `M.armed_open` is set in exactly ONE place -- `M.draw()`
-- above, the SONG_SELECT half -- so on that route it was never set and this function
-- returned false on every frame. Nothing about the movie was broken; it was never armed.
--
-- What the cabinet does on ITS mode-board route is play the WHOLE clip here:
-- `dani_select_all.lua`'s IntroductionMain runs `mc_main.main:GotoAndPlay("in")` and `in`
-- is frame 5, through stop@248 / out@253..262. f5..55 is the full-black scrim (`#1@3`
-- alpha 1.0), i.e. the blackout the player reads as leaving the entry screen, then the
-- kanji stamp, then the reveal. So:
-- __hss_dan_intro == "full" (engine: previous screen was ENTRY) -> arm here, f5..262
-- __hss_dan_intro == "open" (previous screen was SONG_SELECT) -> ROUND 64 f182..262,
-- armed by M.draw()
-- The engine (`DanSelectScreen::on_screen_start`) writes this global once per screen
-- entry; we consume it. A skin/engine pair without it keeps the old behaviour exactly,
-- because `open_from` then falls back to OPEN_FROM.
local INTRO_FROM = 5 -- clip label `in`
-- ── ROUND 95 (r95-danselect-concurrent-intro) ────────────────────────────────
--
-- Reported: 「他現在比較像先載入dan, 才載入段位道場動畫,你要邊放動畫邊讓他同時載入
-- dan」. The engine used to run its (blocking, multi-second) course scan BEFORE
-- arming this movie, so the movie whose entire purpose is to hide the load played
-- after it. `DanSelectScreen::on_screen_start` now arms first and scans on a
-- worker, which means the ribbon behind these doors may still be empty when the
-- clip reaches the reveal.
--
-- f182 (`OPEN_FROM`) is the first frame that shows the player anything behind the
-- doors, so it is the clamp frame -- the exact analogue of ROUND 85's f113 clamp
-- one frame short of the genre-board swap. While `__hss_dan_ready` is false the
-- clip is held on f182 (doors shut, scrim up); the frame it turns true the clock
-- is RE-ANCHORED so f == 182 at that instant and the 80-frame / 1333.3 ms reveal
-- runs from there at 1:1.
--
-- scan wins the race -> nothing is ever clamped, the clip plays f5..f262 (or
-- f182..f262) in full. It is NOT cut short: the cabinet's
-- `Is_EndAnimation` is `mc_main.main:IsPlay() == false`,
-- purely the clip (dani_select_introduction_main.lua).
-- clip wins the race -> hold on f182 for as long as it takes. The cabinet's
-- equivalent wait is `DaniSelectMain`'s per-frame
-- `if daniselectdani_.is_setUpAllOk == true` gate before
-- `timer_:StartCount()` (dani_select_all.lua:126-128).
-- The CLAMP itself is ours, not the cabinet's -- the
-- cabinet's `CreateDaniData` reads a resident datatable
-- and is instant, so it never has anything to hide.
--
-- `DanSelectScreen::tick_timer` derives the credit clock's start from the same
-- three numbers, so the clock and the movie cannot disagree.
-- OLD ENGINE: `__hss_dan_ready` nil is treated as ready, so a skin on a
-- pre-ROUND-95 engine behaves exactly as ROUND 86.
local REVEAL_F = OPEN_FROM -- 182
function M.draw_open()
local req = __hss_dan_intro
if req ~= nil then
__hss_dan_intro = nil
if req == "full" and not M.armed_open then
M.armed_open = true
M.open_t0 = nil
M.open_held = false
M.open_from = INTRO_FROM
-- the cabinet's PlayIntroduction fires TSound "se_daniodai_v12e"/"intro" at
-- the top of IntroductionMain; on the SONG_SELECT route M.draw() already fired
-- it during the close leg and `M.sfx_done` swallows this call.
fire_sfx()
elseif req == "open" then
M.open_from = OPEN_FROM
end
end
if not M.armed_open then return false end
local now = tex.get_current_ms()
if not M.open_t0 then M.open_t0 = now end
local from = M.open_from or OPEN_FROM
local f = from + (now - M.open_t0) / MS_PER_FRAME
-- ROUND 95: clamp at f182 while the courses are still loading, re-anchor on
-- f182 the frame they land. See the block above.
local ready = (__hss_dan_ready ~= false)
if not ready then
if f > REVEAL_F then
f = REVEAL_F
M.open_held = true
end
elseif M.open_held then
M.open_held = false
M.open_t0 = now - (REVEAL_F - from) * MS_PER_FRAME
f = REVEAL_F
end
M.open_f = f -- for the r95 trace (YATAIDON_R95_TRACE)
if f >= OPEN_TO then
M.armed_open = false
M.open_t0 = nil
M.open_held = false
M.open_from = nil
M.sfx_done = false
M.engine_u = nil
M.t0 = nil
return false
end
return draw_frame(f, nil)
end
return M