Files
YataiDONNijiiro/Scripts/global/coin_overlay.lua
T
TaikoZhongandClaude Fable 5.1 c3e5b593d3 Song select: no credit line, course names at the record's size
The cabinet's song select shows no COINS / FREE PLAY line (RESULT keeps it),
and the course names on the difficulty cards are drawn at the record's size 34
instead of the 28 an earlier crop estimate picked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:19:21 +08:00

253 lines
18 KiB
Lua

-- YataiDON-HSS override of PyTaikoGreen/global/coin_overlay.lua
-- Arcade (the reference build) global chrome drawn on every non-game screen:
-- * common/credit : 「フリープレイ」 text, centred (960,1048), skin_config free_play font 34 (see draw)
-- * common/qr__check : QR status chip (106x106 @1570,38) — label ng/ok; YataiDON has no QR -> always NG (frame 0)
-- * common/credit_player_side : 2P cloud bubble 「2人プレイ / 太鼓をたたいてスタート!」 (416x280 @1492,638),
-- shown while the 2P side is not entered (player_num == 1). Blinks in the arcade: 3 s loop, 2 s solid (cloud 0.70/text 1.0) + 0.5 s out + 0.5 s in (see draw).
CoinOverlay = {}
CoinOverlay.__index = CoinOverlay
-- ROUND 8 — the 2P invite cloud's blink is no longer the three hand-written branches
-- round 6 fitted to it: `Scripts/anim/credit_side.lua` is exported straight out of
-- `common/credit_player_side.nulm` sprite 10 (the 180-frame MC the root parks on) by
-- scratchpad/anim_export.py. See ANIM_PIPELINE.md. The export CONFIRMED round 6's
-- model exactly -- cloud `#3@0` 179/256 -> 0 over f0..f30, back over f30..f59, steady
-- f59..f179, and the text `credit_all_instance` the same shape at 256/256 -- so this is
-- provenance and a re-export path, not a visible fix. The one thing that stays a
-- constant is the 8-frame pop-in (root sprite 11 label `main` f9: +32/256 per frame,
-- exactly linear), because a two-row table for a straight line buys nothing.
-- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL
-- clip when a generated table is missing or malformed, so every reader below falls
-- through to its `or <constant>` default instead of taking the screen down.
local _ca_ok, cs_anim = pcall(require, "anim/sampler")
if not _ca_ok then cs_anim = { safe = function()
-- sampler.lua itself is missing: hand back the same NULL clip shape it would
local n = {}
n.at = function() return nil end; n.get = function() return nil end
n.label = function() return nil end; n.has = function() return false end
n.first = function() return 0 end; n.last = function() return 0 end
return n
end } end
local CS = cs_anim.safe("anim/credit_side")
local CS_LEN = math.max(1, CS:last() - CS:first() + 1) -- 180 frames = 3 s (1 if the table is gone)
local CS_POP_F = 8 -- verified against sprite 11 f9..f17
function CoinOverlay.new()
local self = setmetatable({}, CoinOverlay)
-- outline 5 = the arcade record's `border` (ET4 +68); ROUND 12.
-- ROUND 15: 5 was the record's `border` passed raw; OutlinedText scales it by 1.5, so it
-- drew 7.5 px. 3 gives 4.5 px, the closest integer to the cabinet's 5.
self.text = text.create_text("free_play", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 3, 5)
self.config = tex.get_skin_config("free_play")
-- texture names are fixed by the engine's generated TexID table, so arcade art is
-- stored under existing names: banapass_osaifu_keitai = QR chip (0 NG / 1 OK),
-- camera = 2P cloud bubble (both frames identical)
self.qr = tex.load_texture("global/overlay/banapass_osaifu_keitai")
self.bubble = tex.load_texture("global/overlay/camera")
-- ROUND 15 (r15-audit-global): both lines are `credit_player_side.nulm` DefineEditText records
-- ET7 (`text_player`) and ET5 (`text_credit`), and both say size **40**, border **6**,
-- `color="#ffffff"` -- white fill with a black outline, not the dark-grey/no-outline pair
-- round 6 guessed. (Border colour is not in the record -- research/lumen_text.md section 2 --
-- but `movie/attractdemo_cn_tw.wmv` frame 1 shows the same white-fill/black-border treatment
-- on the name plate, which is the same renderer default.) See Graphics/global/MAPPING.md
-- ROUND 15 items G3/G4.
-- OutlinedText multiplies the thickness by screen_scale (`text.cpp:200`,
-- `outline_thickness * global_tex.screen_scale`), which is 1.5 in this skin, while the
-- record's `border` is already in 1080p stage px. Pass 6/1.5 = 4 to draw the arcade's 6.
self.bubble_player = text.create_text("credit_side_player", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2)
self.bubble_msg = text.create_text("credit_side_msg", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2)
self.bubble_cfg_p = tex.get_skin_config("credit_side_player")
self.bubble_cfg_m = tex.get_skin_config("credit_side_msg")
self.blink_t0 = tex.get_current_ms()
return self
end
function CoinOverlay:update(current_ms) end
-- common/credit: root places credit_text_instance at (960,1052.25) [absolute, this lumen's root
-- sits at (0,0)]; `text_credit` = ET4, box **(760,991.75)-(1160,1087.75)** = 400x96, size 40,
-- outline 5, align byte 2 (centre), font stylekey '0' -> size bucket "64" -> **jp_64**.
-- ROUND 12: ROUND 4d sized this off the `face="MS 明朝"` string, which `lumen_fonts.md` §4c
-- proves is inert Flash-authoring residue -- the cabinet has no vector font and picks its atlas
-- from the record's style key alone. The real face is the heavy jp_64 display weight, so 34/3 was
-- ~15 % too small. Arcade pen (lumen_text.md 3/3a, the same rule Graphics/result/MAPPING.md
-- ROUND 6 uses): lineHeight = 92 * 40/64 = 57.5, pen.y = 991.75 + (96-57.5)/2 + 5 = 1016.0, and
-- the katakana ink of 「フリープレイ」 then runs **1023.5..1061.6, centre 1042.56**.
-- Our OutlinedText centres its whole image, whose katakana ink sits 3.0 px above that centre at
-- size 40, so the centre that lands the ink on the cabinet's is 1042.56 + 3.0 = 1045.6 -> 1046.
-- (The arcade's fill is also `border` = 5 px RIGHT of the box centre -- x 965, not 960 -- because
-- FontImpl insets the fill by `border` on both axes; that 5 px is deliberately NOT copied here,
-- see Graphics/loading/MAPPING.md ROUND 12 class C.)
-- OutlinedText adds the skin_config x/y as an offset, so both are subtracted here.
local FREE_PLAY_CY = 1046
-- ROUND 15: the two lines inside the 2P invite cloud, in stage px for the 2P seat
-- (`credit_side_instance_2` at (1700,706)); the 1P seat mirrors x only.
local BUBBLE_PLAYER_CY = 749
local BUBBLE_MSG_CY = 802
-- ROUND 16 (r16-select, QA defect #3 -- the CLIPPING half only; the alpha half was measured and
-- dismissed, see the note at the bottom of draw()).
-- The cabinet centres both lines on the seat (1700 / 220) inside EditText boxes that are WIDER
-- than the screen allows on that side: ET7 `text_player` (1472,694)-(1928,790) = 456 wide and
-- ET5 `text_credit` (1278,747)-(2122,843) = 844 wide, both centred on 1700. At that centre only
-- 2*(1920-1700) = 440 px of a centred string can be on screen at all, and the cloud art itself is
-- 416 px wide (`attract_credit_instance__shape3`, local (-208,-108)..(208,172)).
-- The cabinet gets away with it because its own strings are short: ET5 is 「あと〇コイン入れてね!」
-- and ET7 「〇人プレイ」 (CreditSide.lua Setup swaps in `credit_rest_2p` / `credit_play_2p`).
-- Our localised strings ("Hit the taiko to start!", 「太鼓をたたいてスタート!」) are far wider and ran
-- off both screen edges. FIX: clamp the drawn width to the cloud and let OutlinedText squeeze the
-- glyphs horizontally via `x2` -- the same shrink-to-fit `nameplate.lua` uses for the name box
-- (`draw_in_box`), which is what the cabinet's own renderer does inside an EditText rect.
-- DEVIATION, deliberate: the cabinet never squeezes these two lines because its JP strings fit.
-- The squeeze only engages when a string is wider than the bubble, so JP/short strings are
-- byte-for-byte unchanged; it is localisation headroom, not a re-layout.
local BUBBLE_BOX_W = 416 -- cloud art width; also <= the 440 px a centred string can occupy at x=1700/220
-- Centred, whole-pixel-snapped OutlinedText draw clamped to `box_w` (see nameplate.lua draw_in_box).
local function draw_bubble_line(t, cfg, cx, cy, fade)
if not t or t.width == nil then return end
local w = math.floor(math.min(t.width, BUBBLE_BOX_W) + 0.5)
t:draw({ x = math.floor(cx - w / 2 - cfg.x + 0.5),
y = math.floor(cy - t.height / 2 - cfg.y + 0.5),
x2 = w - t.width,
fade = fade })
end
-- ROUND 73 (r73-danselect-condition-chrome), QA defect 1: 「free play在這畫面不展示」.
--
-- The credit line used to be drawn UNCONDITIONALLY at the top of draw(), while
-- the QR chip and the 2P invite cloud below it both carry explicit screen lists.
-- That is ROUND 71's bug in the other direction: a global overlay member with no
-- screen gate at all, so it leaked onto DAN_SELECT, where the cabinet shows none.
--
-- What the cabinet actually is (the reference build, reference-sourced): the credit line is
-- `AppAlwaysDisplayPartsCredit`, slot 5 of the `AppAlwaysDisplay` parts array
-- (the only part carrying SetAttract/SetEntry/SetGame, vtable +48/+56/+64, which
-- write mode 1/0/2 to this+36). Each scene picks its mode, and the per-scene
-- table (`AppAlwaysDisplay::Scene*`, one folded function per preset) is:
-- SceneTitle / SceneAttractMovie 0x140021FC0 -> +48 SetAttract
-- SceneEntry 0x140022360 -> +56 SetEntry
-- SceneResult 0x140022810 -> +56 SetEntry
-- SceneSongSelect (+ AI variants) 0x140022490 -> +64 SetGame
-- SceneDaniResult 0x140022940 -> +64 SetGame
-- SceneEnsoGame 0x1400226F0 -> +24 FinishDisp (hidden)
-- SceneDaniSelect has NO preset of its own: `SceneDaniSelect.obj.c` inlines the
-- same calls and hits slot 5 with +64 (SetGame) then +8 (StartDisp).
--
-- HONEST LIMIT, recorded rather than papered over: that table does NOT explain
-- this defect. DAN_SELECT and SONG_SELECT take the identical branch (SetGame +
-- StartDisp), yet the cabinet shows the caption on song select and not on dan
-- select, so the real discriminator is somewhere below `SetCreditString`'s
-- mode-2 arm (this+36 == 2, which then keys off the credit counter this+20 and
-- IsFreePlay) and is NOT decoded. The list below is therefore the USER's direct
-- side-by-side observation of the cabinet, written as a screen list so it has
-- the same shape as the two gates underneath it and so the next round can
-- extend it from evidence instead of unpicking a negation.
local CREDIT_SCREENS = {
ENTRY = true,
PRACTICE_SELECT = true, AI_SELECT = true,
TITLE = true, RESULT = true,
-- DAN_SELECT: deliberately absent (QA defect 1).
-- SONG_SELECT / SONG_SELECT_2P: absent -- the cabinet's song select shows no
-- COINS / FREE PLAY line (user, side by side with the cabinet); RESULT keeps it.
}
function CoinOverlay:draw(x, y)
local screen = tex.current_screen and tex.current_screen() or ""
-- Fail soft: an unknown/blank screen name keeps the old always-on behaviour
-- rather than silently blanking the credit line everywhere.
local show_credit = (screen == "") or (CREDIT_SCREENS[screen] == true)
if show_credit then
-- ROUND 11 (text sharpness): whole-pixel snap — an OutlinedText is a rasterised
-- texture drawn 1:1 with BILINEAR, so a .5 destination costs a full 50/50 texel blend.
local text_x = math.floor(tex.get_screen_width() / 2 - self.text.width / 2 - self.config.x + 0.5)
local text_y = math.floor(FREE_PLAY_CY - self.text.height / 2 - self.config.y + 0.5)
self.text:draw({ x = text_x, y = text_y })
end
-- ROUND 5: the engine re-draws this overlay on top of the song-loading (rainbow) transition
-- with tex.in_transition() == true. The arcade loading screen shows ONLY the credit text
-- (no QR chip, no 2P invite cloud), so stop here in that case.
if tex.in_transition and tex.in_transition() then return end
-- Anything a screen script wants drawn ABOVE the credit text (song_select.lua: the course-select
-- drum guide pill, which the arcade layers over the credit line). Consumed and cleared each frame.
if YD_DRAW_ABOVE_CREDIT then
local fn = YD_DRAW_ABOVE_CREDIT
YD_DRAW_ABOVE_CREDIT = nil
fn()
end
-- ROUND 6 (item 46): the arcade title/attract screen carries no status row — the
-- 段位道場 / 1プレイ4曲 / IC Card chips are entry_overlay.lua's and skin_config already
-- parks them off-screen there (entry_overlay_title.x = -4000), but the QR chip is drawn
-- from here, so it needs its own guard.
-- The chip row belongs to the screens where a card can still be tapped: entry and the
-- select screens. Title, the loading transition, result, game over and settings show none.
-- ROUND 73: `screen` is now read once at the top of draw() (the credit gate).
local chip_screens = (screen == "ENTRY" or screen == "SONG_SELECT" or screen == "SONG_SELECT_2P"
or screen == "PRACTICE_SELECT" or screen == "AI_SELECT" or screen == "DAN_SELECT")
if chip_screens and not (tex.in_transition and tex.in_transition()) then
tex.draw_texture(self.qr, { frame = 0 })
end
-- player_num: 1 = 1P alone -> invite on the 2P side (credit_side_instance_2 @1700,706),
-- 2 = 2P alone -> invite on the 1P side (credit_side_instance_1 @220,706), 3 = both -> none
-- The arcade only instantiates credit_side (CreditSide.lua) in entry and song select
-- (entry_main.lua / song_select_all.lua); result, game over, dan, settings never show it.
local scr = (screen ~= "") and screen or "ENTRY"
local on_entry_or_select = (scr == "ENTRY" or scr == "SONG_SELECT" or scr == "SONG_SELECT_2P"
or scr == "PRACTICE_SELECT" or scr == "AI_SELECT")
-- ROUND 6 (2P): `global_data.player_num` is NEVER set to TWO_PLAYER(3) - scenes/entry.cpp
-- forces it back to P1 once the second player joins, so tex.player_num() stays 1 for a
-- 2P session and the invite cloud kept drawing on top of the 2P Don. The two signals the
-- skin does get are the screen name (SONG_SELECT_2P) and entry.lua's draw_footer, which
-- the engine hands p1_joined / p2_joined before it draws this overlay.
local pn = tex.player_num()
local both_joined = (pn == 3) or (scr == "SONG_SELECT_2P")
or (scr == "ENTRY" and YD_ENTRY_BOTH_JOINED == true)
-- ROUND 11 (entry credit screen): entry_main.lua turns `credit_side_[n]` visible only in
-- ModeSelectStartWait -- never during CreditWait -- so the invite cloud must not draw
-- while entry.lua still has the 1人プレイ/2人プレイ pills up (it refreshes
-- YD_ENTRY_CREDIT_MS every frame it draws them, including the decide/fade-out tail).
local credit_up = (scr == "ENTRY") and YD_ENTRY_CREDIT_MS ~= nil
and (tex.get_current_ms() - YD_ENTRY_CREDIT_MS) < 100
-- ROUND 15 (mid-song-select 2P join): `song_select_all.lua` `CreditSideVisible`
-- (l.1622) only turns `credit_side_[n]` visible while `playSongCount < 2`, the
-- same window `Set2PEntryEnable` (l.1806) uses for the join itself. We used to
-- advertise the cloud on every song select, including songs the engine now
-- (correctly) refuses to join on. Entry is unconditional there (entry_main.lua
-- ModeSelectStartWait keys only off is_use_credit), so gate the select screens.
local join_window = (scr == "ENTRY") or not tex.songs_played or (tex.songs_played() < 2)
if on_entry_or_select and join_window and not credit_up and not both_joined and (pn == 1 or pn == 2) then
local side = (pn == 1) and 0 or 1
local cx_p = (pn == 1) and self.bubble_cfg_p.x or (tex.get_screen_width() - self.bubble_cfg_p.x)
local cx_m = (pn == 1) and self.bubble_cfg_m.x or (tex.get_screen_width() - self.bubble_cfg_m.x)
-- ROUND 8: sampled out of `anim/credit_side` (MC 10, 180 frames, no action -> loops
-- forever): the cloud is track `#3@0` (placed at 179/256, so its own row already
-- carries the 0.70) and the text is `credit_all_instance` (256/256). Visible 2 s,
-- blinks away 1 s (fully invisible only on frame 30), 3 s period.
local elapsed = tex.get_current_ms() - self.blink_t0
local f = (elapsed * 0.06) % CS_LEN -- arcade frame 0..179
local pop = math.min(1, elapsed * 0.06 / CS_POP_F) -- 133 ms pop-in on first show
-- ROUND 16 (r16-select): DO NOT "fix" this to 1.0. QA round 16 reported the cloud as
-- "far too transparent, you can read the stalls through it"; measured against the cabinet
-- it is exactly right and opaque would be the regression. `common/credit_player_side.nulm`
-- places shape 3 (the cloud, art fully opaque white) with color_mul [256,256,256,179]
-- => 179/256 = 0.6992, which is the 0.6992 in `anim/credit_side`. `lum.py`'s render of the
-- cabinet's own MC 10 peaks at alpha 178; ours draws 255 * 0.6992 = 178.3. The "ghost"
-- reading is ENTRY-specific: 70 % white over the bright street art is low contrast by
-- design, and the identical draw reads solid over song select's blue backdrop.
local fade = (CS:get("#3@0", f, "a") or 0) * pop
local fade_t = (CS:get("credit_all_instance", f, "a") or 0) * pop
tex.draw_texture(self.bubble, { index = side, fade = fade })
-- OutlinedText adds the skin_config x/y as an offset to params.x/y, so subtract cfg.x/y again
-- ROUND 11: whole-pixel snap (see draw() above)
-- ROUND 15: y is now centred on the arcade's own ink centre instead of being read as a
-- top-left out of skin_config. ET7 box (1472,694)-(1928,790) and ET5 box (1278,747)-(2122,843)
-- for `credit_side_instance_2` at (1700,706); Lumen text is vertically centred in its rect,
-- and the same lineHeight/border correction the free-play line uses (ROUND 12) puts the ink
-- centres at 745.8 and 798.8, +3.0 for our katakana raster = 749 / 802.
-- ROUND 16: clamped to the cloud so a long localised string cannot run off the screen edge
-- (x=1920 on the 2P seat, x=0 on the 1P seat). No-op for a string narrower than the cloud.
draw_bubble_line(self.bubble_player, self.bubble_cfg_p, cx_p, BUBBLE_PLAYER_CY, fade_t)
draw_bubble_line(self.bubble_msg, self.bubble_cfg_m, cx_m, BUBBLE_MSG_CY, fade_t)
end
end