forked from Yonokid/YataiDONNijiiro
entry board titles/comments (entry_* / entry_*_comment_N), costume-menu guide words (guide_select_label / guide_decide_label), the sort window (sort_window_* / folder_sort_select / course_name_*), the dan HUD plate (dan_gauge_name / dan_exam_text_gauge), the N曲 song limit (song_num_max) and the 演奏スキップON badge (skip_badge_text) are read through the engine's tex.skin_text; the only Japanese left in Lua are folder-name matchers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
307 lines
17 KiB
Lua
307 lines
17 KiB
Lua
-- 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
|
|
-- skin_config: dan_gauge_name (魂ゲージ) and dan_exam_text_gauge ("%s %以上", %% = %)
|
|
local fmt = tex.skin_text("dan_exam_text_gauge"):gsub("%%%%", "\1")
|
|
local caption = fmt:gsub("%%s", tostring(ex.red)):gsub("\1", "%%")
|
|
self.label = text.create_raw_text(caption,
|
|
VALUE_SIZE, CAP_FILL, VALUE_OUTLINE, false,
|
|
4.0 / tex.get_screen_scale())
|
|
self.name_label = text.create_raw_text(tex.skin_text("dan_gauge_name"),
|
|
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
|