-- 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)( * 0.5)` at DojoEnsoGraphicTamashiiGage.obj.c:1078, which -- truncates -- but IDA lost the actual argument handed to `to_string` -- 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 self.label = text.create_raw_text(tostring(ex.red) .. "%以上", VALUE_SIZE, CAP_FILL, VALUE_OUTLINE, false, 4.0 / tex.get_screen_scale()) self.name_label = text.create_raw_text("魂ゲージ", 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