251 lines
18 KiB
Lua
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
|