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

251 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, SONG_SELECT = true, SONG_SELECT_2P = true,
PRACTICE_SELECT = true, AI_SELECT = true,
TITLE = true, RESULT = true,
-- DAN_SELECT: deliberately absent (QA defect 1).
}
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