Files
YataiDONNijiiro/Scripts/background/bg_objects/dan_info.lua
T
TaikoZhongandClaude Fable 5.1 638b8b37c5 zh is Simplified Chinese: convert the zh strings, translate the last Japanese-only labels
- every zh string in skin_config.json and the Lua scripts converted Traditional ->
  Simplified (OpenCC TSPhrases + TSCharacters; 裏 kept for 裏魔王); the sort window
  uses the cabinet's own Simplified rows (根据难度选择 / ★的个数 / 排序)
- dan_exam_text_more/less/gauge/gauge_less gain en / zh / ko
- dan_info 魂ゲージ + %以上 and the N曲 song limit follow the interface language
  (cabinet wordlist: clear_theme_soul_gauge, sort_window_songs)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 06:31:22 +08:00

311 lines
18 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- YataiDON-HSS — the 段位道場 exam-panel extras that only Lua can draw.
--
-- ROUND 14. `Background:handle_dan(player, state)` is new this round (see
-- ENGINE_BINDINGS.md): DanGameScreen::update pushes the live exam state into the
-- Background script every frame, which is what MAPPING_dan_info's C-4 and
-- LUA_CAPABILITIES item 56 asked for. `Background:draw_fore()` already ran after
-- the whole GAME_DAN HUD -- it just had nothing to draw with.
--
-- The state table:
-- { dan_color, dan_title, song_index, song_count, total_notes,
-- remaining_notes, gauge, gauge_max,
-- exams = { { type, range, red, gold, value, progress, bar, failed }, ... } }
-- `value` is already normalised the way the engine's own panel prints it (for
-- range="less" it is the REMAINING allowance), `progress` is 0..1.
--
-- WHAT THIS DRAWS, and why each thing is here rather than in the engine's
-- draw_dan_info():
--
-- 1. The 「N %以上」 soul-gauge callout. The cabinet marks the dan's pass
-- threshold on the soul gauge itself (dani_enso: txt_num_border_gauge,
-- MS 明朝 36 #ffffff, right-aligned). `GaugeMode::DAN` has no label slot
-- and `Gauge` exposes no value, so this was unreachable from any skin
-- (MAPPING_dan_info "NOT REACHABLE" 6). The threshold is the `gauge` exam's
-- `red`, which handle_dan now carries, and the gauge bar's own geometry is
-- read out of game/gauge_dan/texture.json rather than written down here.
--
-- Everything else on the panel (row plates, bars, labels, thresholds, live
-- counts, 達成失敗, 残りノーツ数) is still the engine's own draw_dan_info() --
-- this module deliberately does NOT re-implement it. Repainting it from Lua
-- would mean neutralising fifteen engine draws with "y2": 0 and re-baking the
-- 3-sliced bar art; the row pitch, which was the actual arcade mismatch, is
-- fixed properly instead by the new per-screen `dan_exam_info_game` key.
--
-- FAIL SOFT: every read is guarded and every draw is skipped when its input is
-- missing, so a GAME_DAN run on an engine that never calls handle_dan (or with a
-- course that has no `gauge` exam) is exactly what it was before this file.
local M = {}
M.__index = M
-- The dan soul gauge, from Graphics/game/gauge_dan/texture.json: `1p_bar` sits at
-- (726, 248) and the bar art is 1080x48.
--
-- CORRECTED BY ROUND 100 -- the old line here read "i.e. 100 % spans x 726..1806"
-- and the threshold was placed with `GAUGE_X + GAUGE_W * pct/100`. That is the
-- ART's json base and PNG width, NOT the gauge's geometry. `Gauge::draw`'s DAN
-- branch (src/objects/game/gauge.cpp:652-665, ROUND 19) draws the fill on
-- kDanSegs = 50, kDanPitch = 21.0, kDanX0 = 738
-- so the gauge is 50 cells of 21 px running 738..1788 -- pitch 21.0, not
-- 1080/50 = 21.6, and starting 12 px right of the art base. Measured live on
-- scratchpad/r100/shots/before/gd_100.png: the olive groove runs x 738..1781.
-- The `overlay.png` separator art agrees independently: 49 bars, pitch EXACTLY
-- 21.000, texture columns 33+21k..38+21k -> screen 738+21k..743+21k (k = 1..49).
--
-- Consequence of the old formula: at 100 % it put the marker at 1806, i.e. 18 px
-- PAST the end of the gauge -- the red arrow visibly pointed at empty space to
-- the right of the bar (before_right_end_x3.png). The error was 0 near the
-- middle and grew to ~29 px at the ends, because the pitch was 2.9 % too wide.
local GAUGE_Y = 248
local GAUGE_X0 = 738 -- gauge.cpp kDanX0: cell 1 starts here
local CELL_PITCH = 21 -- gauge.cpp kDanPitch
local CELL_COUNT = 50 -- gauge.cpp kDanSegs
local LABEL_SIZE = 30 -- arcade MS 明朝 36; ours reads heavier
-- `enso_dani/dani_select/dani_select` id 27 `txt_num_border_gauge`: size 36,
-- border 4.0, align right. ROUND 16 (r16-outline): a Lumen `border` is already
-- in 1080p stage px and OutlinedText multiplies the thickness by screen_scale
-- (1.5), so it is divided out here; this used to pass 3 and draw 4.5 px.
local WHITE = { 255, 255, 255, 255 }
local BLACK = { 0, 0, 0, 255 }
-- ── ROUND 64b (r64-danselect-fidelity, GAME_DAN half) ───────────────────────
-- The report: 「右上方的"魂Gauge 100%以上" 繪製錯誤」. The callout above the dan
-- soul gauge is not bare text on the cabinet -- it is a PLATE.
--
-- Source: the reference build `enso_dani/enso/tamashiigauge/tamashii_gauge.{nulm,nutexb}`,
-- root timeline (`lumen_anim_dump --list`: `tx_border` char 141, `tx_tamashii`
-- char 143; `lumen_shape_dump -m -O 960 540 -f 24 -c -v`):
--
-- shape 137 @ (-445,-39) a 586x60 light-grey rounded plate carrying a cream
-- rounded caption pill in its left half AND the red
-- down-arrow threshold marker baked at its right end
-- (the arrow tracks the plate in every frame of the
-- movie, which is how it was identified as one shape)
-- id 141 @ (-432,-79.5) size 36, align 1 (CENTRE), rgba ffffff -- "100%以上"
-- id 142 @ (-718,-74.5) size 26, align 2, rgba ffffff -- "魂ゲージ"
-- shape 139 @ (-176, 16) a 28x61 white/black-outlined cell that replaces the
-- gauge's own cell at the threshold. DECODED, NOT
-- PORTED: our dan gauge art has its own cell pitch,
-- so dropping the cabinet's cell onto it would land
-- between cells. Extracted for a later round as
-- game/dan_info/gauge_border_cell.png.
--
-- The plate's RIGHT end (where the arrow is) is the threshold position, so the
-- whole plate hangs to the LEFT of it with its bottom on the gauge's top edge.
-- Art extracted verbatim by scratchpad/r64/extract_gauge.py.
local PLATE_W, PLATE_H = 586, 60
--
-- ROUND 90 (r90-danplate-brown-and-place) -- ALL FOUR NUMBERS BELOW RE-MEASURED.
--
-- ROUND 64b read its placements off the ROOT timeline at frame 24, which is
-- inside the movie's `dani_result` region, and expressed them as raw stage
-- coordinates. This round reads sprite 145 (`gauge_border`) at ITS OWN frame 0,
-- the `dani_enso` label -- the state this screen is actually in -- rendered with
-- lumen_shape_dump ... -m -r 145 -f 0 -c -v -O 960 540
-- so every number is SPRITE-LOCAL with the threshold cell (shape 139, placed at
-- exactly (0,0)) as the origin. The relative geometry agrees with ROUND 64b to
-- the pixel; what changes is where that geometry is ANCHORED.
--
-- plate body (#EAEAEA) sprite-local x -562 .. +24, y -85 .. -26 (586 x 60)
-- gauge bar (sprite 146, gauge_mc) bar top = 146-local y +3, 33 px tall
-- the cell origin is 146-local (x, +16), so plate top = bar_top - 72
-- plate btm = bar_top - 12
--
-- Two consequences, and they are the user's items 1 and 3:
--
-- * X: the plate's right end is NOT the threshold -- it overhangs it by 24 px.
-- The red arrow baked into the plate is centred at plate-local 561.5, i.e.
-- ON the threshold, and 24 px of rounded plate end continues past it. Ours
-- put the plate's right EDGE on the threshold, so the arrow pointed 24 px
-- left of the cell it marks.
-- * Y: the plate does not sit ON the gauge, it floats 12 px above it. Ours had
-- `py = GAUGE_Y - PLATE_H`, i.e. flush. That is the reported 「該稍微上移」.
local PLATE_GAP = 12 -- px between the plate's bottom and the bar top
local PLATE_OVERHANG = 24 -- px the plate extends PAST the threshold
--
-- The two captions, from the same render (ink bounding boxes of the real glyphs,
-- measured against a no-font render of the same frame so only glyph pixels are
-- counted; the JP-path problem is solved with a directory junction, see
-- MAPPING_dan_info ROUND 90):
--
-- tx_tamashii (id 142, align 2 = CENTRE) box centre plate-local x 147,
-- glyph ink -463..-361 x -78..-53 -> ink centre plate-local (150, 19.5)
-- tx_border (id 141, align 1 = RIGHT) box right edge plate-local 532,
-- glyph ink -188..-26 x -80..-46 -> ink centre y plate-local 22
--
-- CORRECTION TO ROUND 64b, which recorded id 141 as "align 1 (CENTRE)": it is
-- not centred. Its 228 px box spans plate-local 304..532 (centre 418) while the
-- 「100%以上」 ink measures 374..536 -- flush with the box's RIGHT edge, 44 px
-- right of the box centre. align 1 is RIGHT. (ROUND 64b was still right that the
-- old code's bug was right-aligning to the THRESHOLD, 54 px further right again.)
local PILL_CX, PILL_CY = 150, 20 -- 魂ゲージ ink centre, plate-local
local VALUE_RIGHT = 532 -- 100%以上 is RIGHT-aligned to this
local VALUE_CY = 22 -- its ink centre y, plate-local
local NAME_SIZE = 26 -- arcade id 142
local VALUE_SIZE = 36 -- arcade id 141
--
-- The outline colour. See MAPPING_dan_info ROUND 90 §1 for the full derivation
-- and for the correction it makes to ROUND 78 §5.
--
-- Short version: the record itself carries no border colour (`+50 == 0`), so
-- `AppLumenRenderer::RenderText` takes the renderer default `this[1102]`, which
-- `SetTextColor` sets to pure black for a non-black fill -- ROUND 78, still true.
-- But RenderText then runs that colour through the PlaceObject colour transform
-- (`out = colour * Cmul + Cadd`, AppLumenRenderer.obj.c:1265-1282), and the
-- caption's wrapper sprite `tx_tamashii` (char 143) carries
-- cadd = [80, 60, 45, 0] (= tamashii_gauge.nulm styles[63], #503C2D)
-- while `tx_border` next to it carries cadd [0,0,0,0]. So:
-- outline = BLACK * 1 + (80,60,45) = #503C2D BROWN
-- fill = WHITE * 1 + (80,60,45) = saturates back to #FFFFFF
-- which is exactly why the add is invisible except on the outline, and exactly
-- why the user reported the pill caption as black-when-it-should-be-brown while
-- saying nothing about the 「100%以上」 next to it.
local CAP_FILL = { 255, 255, 255, 255 }
local CAP_OUTLINE = { 80, 60, 45, 255 } -- #503C2D, the colour ADD, measured
local VALUE_OUTLINE = { 0, 0, 0, 255 } -- cadd [0,0,0,0] -> stays pure black
function M.new()
return setmetatable({ state = nil, label = nil, label_for = nil,
name_label = nil, plate = nil, plate_tried = false }, M)
end
function M:plate_id()
if not self.plate_tried then
self.plate_tried = true
self.plate = tex.load_texture("game/dan_info/gauge_border_plate")
end
return self.plate
end
-- Called from Background:handle_dan.
function M:handle(state)
self.state = state
end
local function gauge_exam(state)
if not state or not state.exams then return nil end
for _, e in ipairs(state.exams) do
if e.type == "gauge" then return e end
end
return nil
end
-- Called from Background:draw_fore(), i.e. after the whole GAME_DAN HUD.
function M:draw()
local st = self.state
if not st then return end
local ex = gauge_exam(st)
if not ex or not ex.red or ex.red <= 0 then return end
-- ── ROUND 100 (r100-danplate-anchor) ────────────────────────────────────
-- The threshold, on the gauge's own 50-cell grid.
--
-- The cabinet does NOT compute a pixel position at all. Its owner,
-- `App::DojoEnsoGraphicTamashiiGage`, contains ZERO position/transform API
-- calls -- the complete list of `lumen::` symbols in the whole 60-function
-- module is CalcHash / BaseMovieInterface(::inner_type) /
-- DisplayList::search_descendant / DynamicText(Interface)(::set_text) /
-- MovieClipInterface::{goto_label,play,search_child,search_descendant,stop} /
-- Player::root_mc / Ptr / String / WeakPtr. All it does is
-- goto_label(gauge_mc, "border_" .. N); stop(gauge_mc);
-- (DojoEnsoGraphicTamashiiGage.obj.c:1081-1097), so the movie's own frame is
-- the entire answer.
--
-- And the movie MOVES THE WHOLE PLATE. `enso_dani/enso/tamashiigauge/
-- tamashii_gauge.nulm` MC 146 (`gauge_mc`) has 51 frames labelled
-- border_0..border_50 -- the gauge is in 2 % units -- and the instance
-- `gauge_border` (the plate, both captions and the baked red arrow, all one
-- child) is placed at
-- tx = -575 + 21*(N-1) N >= 1 (border_0 aliases to border_1)
-- i.e. -575, -575, -554, -533 ... -71 (50 %) ... 454 (100 %). Exactly the
-- 21 px pitch of the sibling `glow_1..glow_50` gauge cells, which sit at
-- -574 + 21*(N-1). Walked in scratchpad/r100/border_track2.py; cross-checked
-- in pixels against r90's own s146_f0/f25 renders (plate bodies 504 px
-- apart, JSON says -71 - -575 = 504 -- exact).
--
-- SO THRESHOLD-TRACKING IS THE CABINET'S RULE, NOT OUR INVENTION. A 50 %以上
-- course really does render this plate mid-gauge on a reference cabinet.
-- Do not "fix" it to the right; see MAPPING_dan_info ROUND 100.
--
-- What IS ported here that was not before:
-- * the 2 % QUANTISATION. The cabinet has 51 discrete frames and nothing in
-- between, so N = threshold/2. This matters: 13 of the 25 shipped courses
-- have an ODD threshold (91/93/95/97/99).
-- UNVERIFIED: whether the cabinet floors or rounds. Floor is used because
-- the one place the reference source still shows the arithmetic is an int cast,
-- `(int)(<field> * 0.5)` at DojoEnsoGraphicTamashiiGage.obj.c:1078, which
-- truncates -- but IDA lost the actual argument handed to `to_string<int>`
-- at :1082, so this is an INFERENCE, not a read.
-- * the 21 px pitch and the 738 origin (see the header block).
--
-- `x` is the plate's ANCHOR = `gauge_border`'s origin, which the movie puts
-- at the CENTRE of the marker cell: sprite 145 places shape 139 (the 28x61
-- replacement cell) at exactly (0,0) and that shape's local box is
-- -16..+16, so its centre IS the origin. The cabinet's marker tile lands at
-- MC146-local -591..-559 for N=1 while the glow cell's tile is -590..-558 --
-- the same slot, nudged 1 px left, which is the `- 1` below.
local pct = ex.red
local n = math.floor(pct / 2)
if n < 1 then n = 1 end -- border_0 == border_1 on the cabinet
if n > CELL_COUNT then n = CELL_COUNT end -- dan.json may say >100 %; the cabinet cannot
local x = GAUGE_X0 + CELL_PITCH * n - CELL_PITCH / 2 - 1
if self.label_for ~= ex.red then
-- cabinet wordlist: clear_theme_soul_gauge = 魂ゲージ / Soul Gauge / 魂槽 / 클리어 게이지
local lang = (tex.language and tex.language()) or "ja"
local W = {
ja = { "魂ゲージ", "%以上" }, en = { "Soul Gauge", "% or more" },
zh = { "魂槽", "%以上" }, ko = { "클리어 게이지", "% 이상" },
}
local w = W[lang] or W.ja
self.label = text.create_raw_text(tostring(ex.red) .. w[2],
VALUE_SIZE, CAP_FILL, VALUE_OUTLINE, false,
4.0 / tex.get_screen_scale())
self.name_label = text.create_raw_text(w[1],
NAME_SIZE, CAP_FILL, CAP_OUTLINE, false,
3.0 / tex.get_screen_scale())
self.label_for = ex.red
end
-- ROUND 64b / re-anchored ROUND 90: the plate. Clamped so a very low
-- threshold cannot push the plate off-screen left.
local pid = self:plate_id()
-- ROUND 90: the plate overhangs the threshold by 24 px (the arrow, baked at
-- plate-local 561.5, is what lands ON it) and floats 12 px above the bar.
-- ROUND 100: `px` is a pure function of `ex.red`, and `ex.red`'s ONLY writer
-- is dan_select.cpp:949 (`sd.selected_dan_exam = current->exams`) on leaving
-- DAN_SELECT -- game_dan.cpp only ever reads it. So the plate cannot move
-- inside a course. Measured, not just argued: 432,240 draw calls across a
-- whole 3-song course, both song transitions included, produced exactly ONE
-- distinct (px, py). See MAPPING_dan_info ROUND 100 §B.
local px = math.max(0, math.floor(x - PLATE_W + PLATE_OVERHANG + 0.5))
local py = GAUGE_Y - PLATE_GAP - PLATE_H
if pid then
tex.draw_texture(pid, { x = px, y = py })
end
-- FAIL SOFT: with no plate art the two captions still draw, at the same
-- places, so this degrades to (a better-placed) version of what it replaced
-- rather than to nothing.
if self.name_label then
self.name_label:draw({
x = math.floor(px + PILL_CX - self.name_label.width / 2 + 0.5),
y = math.floor(py + PILL_CY - self.name_label.height / 2 + 0.5),
})
end
if self.label then
-- ROUND 90: `tx_border` is align = 1 = RIGHT, and its box's right edge is
-- plate-local 532 -- 30 px INSIDE the threshold, not at it. ROUND 64b
-- read it as centred; the measured glyph ink says otherwise (see the
-- header). The pre-ROUND-64b bug was right-aligning to the threshold
-- ITSELF (plate-local 586), 54 px further right again, so this is not a
-- revert of that fix.
self.label:draw({
x = math.floor(px + VALUE_RIGHT - self.label.width + 0.5),
y = math.floor(py + VALUE_CY - self.label.height / 2 + 0.5),
})
end
end
return M