Required changes #2

Merged
GanYiZhong merged 3 commits from Yonokid/YataiDON14:main into main 2026-10-15 15:52:16 +02:00
25 changed files with 2624 additions and 387 deletions

No files matched your search

Binary file not shown.
+89 -93
View File
@@ -1,94 +1,90 @@
{
"bubble": {
"x": 0,
"y": 0
},
"counter": {
"x": 0,
"y": 0,
"crop": [
[
0,
0,
1.5,
1.5
],
[
1.5,
0,
1.5,
1.5
],
[
3,
0,
1.5,
1.5
],
[
4.5,
0,
1.5,
1.5
],
[
6,
0,
1.5,
1.5
],
[
7.5,
0,
1.5,
1.5
],
[
9,
0,
1.5,
1.5
],
[
10.5,
0,
1.5,
1.5
],
[
12,
0,
1.5,
1.5
],
[
13.5,
0,
1.5,
1.5
]
]
},
"pop": [
{
"x2": 0,
"y2": 0
},
{
"x2": 0,
"y2": 0
}
],
"rainbow": {
"x": 189,
"y": -174
},
"rainbow_mask": {
"x": 189,
"y": -174
},
"explosion": {
"x2": 0,
"y2": 0
}
{
"bubble": {
"x": 0,
"y": 0
},
"counter": {
"x": 0,
"y": 0,
"crop": [
[
0,
0,
1.5,
1.5
],
[
1.5,
0,
1.5,
1.5
],
[
3,
0,
1.5,
1.5
],
[
4.5,
0,
1.5,
1.5
],
[
6,
0,
1.5,
1.5
],
[
7.5,
0,
1.5,
1.5
],
[
9,
0,
1.5,
1.5
],
[
10.5,
0,
1.5,
1.5
],
[
12,
0,
1.5,
1.5
],
[
13.5,
0,
1.5,
1.5
]
]
},
"pop": [
{
"x2": 0,
"y2": 0
},
{
"x2": 0,
"y2": 0
}
],
"rainbow": {
"x": 189,
"y": -174
},
"explosion": {
"x2": 0,
"y2": 0
}
}
+43 -283
View File
@@ -4,132 +4,58 @@
"width": 960,
"height": 720,
"options": {
"delay_score_addition": true,
"horizontal_song_select": false,
"one_menu_sort": false,
"option_skip_row": true,
"option_neiro_row": false,
"dan_result_rank_plate": false,
"dan_game_rank_plate": false,
"dan_select_rank_plate": false,
"entry_credit_arcade": true,
"dan_confirm_yes_left": false,
"costume_preset_slots": false,
"dan_title_horizontal": false,
"dan_chip_name_black": false,
"dan_exam_grid": false,
"dan_select_total_notes": true,
"dan_exam_value_center": true,
"combo_color_tiers": false,
"gauge_cell_fade_in": false,
"disable_chara_3d": true
"disable_chara_3d": true,
"loading_countdown": true
}
},
"balloon_counter_2p_offset": {
"game_balloon_counter_2p_offset": {
"x": 0,
"y": 99
},
"balloon_counter_margin": {
"game_balloon_counter_margin": {
"x": 36
},
"balloon_offset": {
"game_balloon_offset": {
"x": 0
},
"branch_indicator_y_offset": {
"game_branch_indicator_y_offset": {
"y": 0
},
"chara_entry": {
"x": 0,
"y": 2000
},
"combo_announce_add_x": {
"x": 409.5
},
"combo_announce_hundreds_offset": {
"x": 0
},
"combo_announce_number_thousands_x": {
"x": -48
},
"combo_announce_thousands_add_x": {
"x": 265.5
},
"combo_announce_thousands_offset": {
"x": 0
},
"combo_margin": {
"x": 30,
"y": 42
},
"dan_chip_name": {
"dan_select_chip_name": {
"x": 42.3,
"y": 159.1,
"height": 375.3,
"font_size": 36
},
"dan_exam_info": {
"dan_select_exam_info": {
"width": 846.0,
"x": 19.8,
"y": 80
},
"dan_exam_info_game": {
"game_dan_exam_info": {
"x": 16.5,
"y": 96,
"width": 705
},
"dan_exam_value_center": {
"x": 295,
"width": -5
},
"dan_game_hori_name": {
"x": 115.5,
"y": 433.5,
"width": 206.25
},
"dan_hori_name": {
"x": 296.6,
"y": 118.6,
"width": 247.5
},
"dan_level_counter_margin": {
"x": 9.0
},
"dan_result_chara": {
"x": 100,
"y": 322
},
"dan_result_nameplate": {
"x": 296,
"y": 3,
"height": 200.0
},
"dan_ribbon": {
"x": 440.6,
"width": 135.0
},
"dan_ribbon_side": {
"x": 296.6,
"y": 288.4
},
"dan_select_indicator": {
"x": 260,
"y": 560
},
"dan_subtitle": {
"dan_select_subtitle": {
"x": 464.0,
"y": 515.5,
"height": 360.0,
"font_size": 27
},
"dan_title": {
"dan_select_title": {
"x": 504.5,
"y": 157.3,
"height": 360.0,
"font_size": 36
},
"dan_yellow_box_offset": {
"x": 126.0
},
"diff_tower_easy": {
"text": {
"ja": " ",
@@ -175,32 +101,18 @@
"ko": " "
}
},
"drumroll_counter_margin": {
"game_drumroll_counter_margin": {
"x": 42
},
"drumroll_width_offset": {
"game_drumroll_width_offset": {
"width": -108
},
"entry_box_highlight_offset": {
"x": -30
},
"entry_box_spacing": {
"x": 4
},
"entry_box_text": {
"font_size": 32
},
"entry_box_text_offset": {
"y": 29
},
"entry_chara_offset_l": {
"x": 250,
"y": 965
},
"entry_chara_offset_r": {
"x": 250,
"y": 965
},
"entry_costume": {
"text": {
"en": "",
@@ -208,25 +120,7 @@
"zh": "",
"ko": ""
}
},
"entry_drum_offset": {
"x": 0,
"y": 480
},
"entry_overlay_entry": {
"x": 0,
"y": -2000
},
"entry_overlay_title": {
"x": 0,
"y": -2000
},
"exam_counter_margin": {
"x": 18.0
},
"exam_gauge_offset": {
"x": -7.2
},
},␍
"free_play": {
"x": 0,
"y": 682,
@@ -238,42 +132,18 @@
"ko": "프리 플레이"
}
},
"game_chara_balloon": {
"x": -3000,
"y": 0
},
"game_chara_p1": {
"x": -3000,
"y": 0
},
"game_chara_p2": {
"x": -3000,
"y": 0
},
"game_nameplate_1p": {
"x": -6000,
"y": 0
},
"game_nameplate_2p": {
"x": -6000,
"y": 0
},
"game_p2_y": {
"y": 346.5
},
"gauge_cells": {
"game_gauge_cells": {
"x": 50
},
"gauge_hit_effect_note": {
"game_gauge_hit_effect_note": {
"x": 814.5,
"y": -138,
"height": 384
},
"genre_bg_title": {
"y": -2000,
"width": 225
},
"gogo_fire_fade": {
"game_gogo_fire_fade": {
"x": 1.0
},
"hit_taiko_to_start": {
@@ -288,23 +158,6 @@
"ko": " "
}
},
"indicator_don_offset": {
"x": 192.6
},
"indicator_entry_left": {
"x": 0,
"y": 2000
},
"indicator_entry_right": {
"x": 0,
"y": 2000
},
"indicator_face_offset": {
"x": 157.5
},
"indicator_kat_offset": {
"x": 20.7
},
"indicator_text": {
"x": 101.7,
"font_size": 23,
@@ -315,27 +168,7 @@
"ko": "테두리로 선택 면으로 결정"
}
},
"judge_counter_1": {
"x": 228.15,
"y": 453.375
},
"judge_counter_2": {
"x": 157.95,
"y": 490.29
},
"judge_counter_3": {
"x": 236.925,
"y": 528.18
},
"judge_counter_4": {
"x": 528.18,
"y": 411.48,
"width": 590.04
},
"judge_counter_margin": {
"x": 20.175
},
"judge_pos": {
"game_judge_pos": {
"x": 210,
"y": 96
},
@@ -555,62 +388,24 @@
"height": 33,
"font_size": 4
},
"moji": {
"game_moji": {
"x": 0,
"y": 148.5
},
"moji_drumroll": {
"game_moji_drumroll": {
"x": 71.25,
"y": 36,
"width": 0
},
"nameplate_entry": {
"x": 0,
"y": 2000
},
"nameplate_entry_left": {
"x": 0,
"y": 2000
},
"nameplate_entry_right": {
"x": 0,
"y": 2000
},
"neiro_text": {
"x": 176.25,
"y": 844.5,
"font_size": 37.5
},
"note_arc_curve_height": {
"height": 244.5
},
"note_arc_start_x_offset": {
"x": 150
},
"notes": {
"game_notes": {
"y": 36
},
"practice_menu_panel": {
"y": 188.0001,
"width": 705,
"height": 345
},
"practice_progress_bar_width": {
"game_practice_progress_bar_width": {
"width": 915
},
"score_counter_2p_y_offset": {
"game_score_counter_2p_y_offset": {
"y": 213
},
"score_counter_animation_start_y": {
"y": -91.5
},
"score_counter_margin": {
"x": 30
},
"score_counter_pos": {
"x": 913.5,
"y": -16.5
},
},␍
"skip_counter": {
"x": 480,
"y": 600,
@@ -629,33 +424,24 @@
"y": 600,
"font_size": 33
},
"song_box_bg": {
"song_select_box_bg": {
"width": 59
},
"song_box_name": {
"song_select_box_name": {
"x": 48,
"y": 136,
"height": 416,
"font_size": 32
},
"song_info": {
"game_song_info": {
"x": 915,
"y": 355.5,
"font_size": 36,
"outline": 0
},
"song_info_max_width": {
"game_song_info_max_width": {
"width": 555
},
"song_info_result": {
"x": -3000,
"y": -3000,
"font_size": 24
},
"song_info_result_subtitle": {
"y": -3000,
"font_size": 20
},
"song_num": {
"x": -2000,
"y": 28,
@@ -667,47 +453,21 @@
"ko": "{0}곡째"
}
},
"song_num_result": {
"x": -3000,
"y": -3000
"chara_3d": {
"entry": [0, 2000],
"song_select_1p": [0, 2000],
"song_select_2p": [0, 2000],
"dan_result": [100, 322],
"game_p1": [-3000, 0],
"game_p2": [-3000, 0],
"game_balloon": [-3000, 0]
},
"song_select_chara_1p": {
"x": 0,
"y": 2000
},
"song_select_chara_2p": {
"x": 0,
"y": 2000
},
"song_select_indicator": {
"x": 0,
"y": 2000
},
"song_select_nameplate_1p": {
"x": 0,
"y": 2000
},
"song_select_nameplate_2p": {
"x": 0,
"y": 2000
},
"song_select_offset": {
"x": -237
},
"total_notes_counter_margin": {
"x": 22.5
},
"yb_diff_offset": {
"x": 60,
"y": -20
},
"yellow_box_bottom": {
"y": 528
},
"loading_countdown": {
"x": 480.0,
"y": 530.0,
"width": 24.75,
"height": 10
"nameplate": {
"entry": [0, 2000],
"song_select_1p": [0, 2000],
"song_select_2p": [0, 2000],
"dan_result": [296, 3],
"game_p1": [-6000, 0],
"game_p2": [-6000, 0]
}
}
+3 -3
View File
@@ -21,8 +21,8 @@ geometry is multiplied by 1.5:
* animation.json: `total_distance, start_position, waypoint`.
* skin_config: `x, y, width, height, font_size`.
Durations, frame counts, alpha, sizes-as-ratios, `outline`, `gauge_cells` (a count) and
`gogo_fire_fade` (a flag) are left as they are.
Durations, frame counts, alpha, sizes-as-ratios, `outline`, `game_gauge_cells` (a count) and
`game_gogo_fire_fade` (a flag) are left as they are.
Lua scripts are copied **verbatim**. Nantoka's Lua is written in 640×480 coordinates, so its
Lua-drawn parts are misplaced until those scripts become resolution-independent
@@ -35,7 +35,7 @@ Lua-drawn parts are misplaced until those scripts become resolution-independent
* `Graphics/background/footer` comes from Juuyon, because `settings.cpp` draws it. Every other background subset is Nantoka's. Juuyon's `background/renda` and enso backgrounds are dropped.
* `Scripts/global`: `transition.lua` and `result_transition.lua` are Nantoka's. `timer.lua` (song select) is Juuyon's.
* `Sounds`: `hit_sounds/` is Nantoka's (in-game drum). The top-level `don.wav`/`ka.wav` (menu drum) are Juuyon's.
* `skin_config.json`: `screen` is Juuyon's, except that the enso/result options (`delay_score_addition`, `dan_game_rank_plate`, `combo_color_tiers`, `gauge_cell_fade_in`) are Nantoka's. The per-key owner table is `KEY_OWNER` in the builder. A game key that only Juuyon defines (`branch_indicator_y_offset`) is dropped, so it falls back to the parent (×0.75, the same as Nantoka's own fallback ×0.5 scaled by 1.5).
* `skin_config.json`: `screen` is Juuyon's, except that the enso/result options (`delay_score_addition`, `dan_game_rank_plate`, `combo_color_tiers`, `gauge_cell_fade_in`) are Nantoka's. The per-key owner table is `KEY_OWNER` in the builder. A game key that only Juuyon defines (`game_branch_indicator_y_offset`) is dropped, so it falls back to the parent (×0.75, the same as Nantoka's own fallback ×0.5 scaled by 1.5).
* Patch applied to this copy only: Juuyon's `song_select.lua` `draw_selector` sets the Lua global `NK_SELECTED_DIFF`, which Nantoka's transition iris reads for its per-course colour.
## Rebuild
+1 -1
View File
@@ -445,7 +445,7 @@ end
-- stripes only, so it is stretched from the song-title bar (y 255) to the bottom)
-- nk_dan/footer = background/collab/dan/footer/0 (blue band), y 448..480
-- nk_dan/remaining_box = that notice board (bg_0 15..282 x 103..304, halved), under "残り音符数".
-- Layout (Graphics/game/dan_info + skin_config dan_exam_info / dan_game_hori_name): the three exam
-- Layout (Graphics/game/dan_info + skin_config dan_select_exam_info / dan_game_hori_name): the three exam
-- rows share y 255..448 evenly (tops 263.5 / 327.5 / 391.5, step 64); the rank plate + board group
-- (150 high) is centred in the same span.
local DAN_PANEL = { top = 255, footer_y = 448, box_x = 6.5, box_y = 323.5 }
+431
View File
@@ -0,0 +1,431 @@
local SKIN = {
total_notes_counter_margin = { x = 22.5 },
exam_counter_margin = { x = 18 },
dan_yellow_box_offset = { x = 126 },
dan_level_counter_margin = { x = 9 },
dan_hori_name = { x = 296.6, y = 118.6, width = 247.5 },
dan_exam_value_center = { x = 295, width = -5 },
yellow_box_bottom = { y = 528 },
}
DanSelect = {}
DanSelect.__index = DanSelect
local _tex = {}
local function T(subset, name)
local key = subset .. "/" .. name
local id = _tex[key]
if id == nil then
id = tex.get_texture("dan_select/" .. subset .. "/" .. name)
_tex[key] = id
end
return id
end
local function cfg(name)
return tex.get_skin_config(name)
end
-- Texture draw-offset at index 0 (TextureObject::x[0] / y[0] / x2[0]).
local function tex_x0(subset, name)
local info = tex.get_texture_info(subset, name)
if info and info.x and info.x[1] then return info.x[1] end
return 0
end
local function tex_y0(subset, name)
local info = tex.get_texture_info(subset, name)
if info and info.y and info.y[1] then return info.y[1] end
return 0
end
local function tex_x2(subset, name)
local info = tex.get_texture_info(subset, name)
if info and info.x2 and info.x2[1] then return info.x2[1] end
return 0
end
-- exam.type -> icon texture name (exam_icon_id in box_dan.cpp). "renda" resolves to
-- exam_roll, falling back to exam_drumroll in older skins. Returns the resolved name.
local EXAM_ICON = {
gauge = "exam_gauge", combo = "exam_combo",
hit = "exam_hit", judgebad = "exam_judgebad",
judgegood = "exam_judgegood", judgeperfect = "exam_judgeperfect",
score = "exam_score", renda = "exam_roll",
}
local function exam_icon_name(type)
local name = EXAM_ICON[type]
if not name then return nil end
if T("yellow_box", name) then return name end
if T("yellow_box", "exam_drumroll") then return "exam_drumroll" end
return nil
end
local yb_box = { path = nil, state = "closed" }
function DanSelect.new()
local self = setmetatable({ now = 0 }, DanSelect)
yb_box.path, yb_box.state = nil, "closed"
return self
end
local YB_DIFF_ANIMS = { 13, 14, 15, 16, 17, 18 }
local function draw_dan_yellow_box(is_diff, fade)
local right_x = tex.get_animation(is_diff and 14 or 10).attribute
local left_x = tex.get_animation(is_diff and 13 or 9).attribute
local center_width = tex.get_animation(is_diff and 15 or 11).attribute
local top_y = tex.get_animation(16).attribute
local center_height = tex.get_animation(17).attribute
local bottom_y = (SKIN.yellow_box_bottom or { y = 0 }).y
local info = tex.get_texture_info("yellow_box", "yellow_box_bottom_right")
local eh = (info and info.height) or 0
tex.draw_texture(T("yellow_box", "yellow_box_bottom_right"), { x = right_x, y = bottom_y, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_bottom_left"), { x = left_x, y = bottom_y, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_top_right"), { x = right_x, y = top_y, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_top_left"), { x = left_x, y = top_y, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_bottom"), { x = left_x + eh, y = bottom_y, x2 = center_width, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_right"), { x = right_x, y = top_y + eh, y2 = center_height, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_left"), { x = left_x, y = top_y + eh, y2 = center_height, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_top"), { x = left_x + eh, y = top_y, x2 = center_width, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_center"), { x = left_x + eh, y = top_y + eh, x2 = center_width, y2 = center_height, fade = fade })
end
-- Track the open box and start the diff-select expansion on the closed -> diff_select
-- transition (the dan box never passes through the "open" draw_state).
local function track_dan_yellow_box(box)
local state = box:draw_state()
local path = box:path()
if state == "closed" then
if yb_box.path == path then
yb_box.path, yb_box.state = nil, "closed"
end
return
end
if yb_box.path ~= path then
yb_box.path, yb_box.state = path, state
for _, id in ipairs(YB_DIFF_ANIMS) do tex.get_animation(id):reset() end
for _, id in ipairs(YB_DIFF_ANIMS) do tex.get_animation(id):start() end
end
end
local function update_dan_yellow_box(ms)
for _, id in ipairs(YB_DIFF_ANIMS) do tex.get_animation(id):update(ms) end
end
function DanSelect:update(ms)
self.now = ms
update_dan_yellow_box(ms)
end
local function draw_tex(subset, name, params)
local id = T(subset, name)
if id then tex.draw_texture(id, params) end
end
-- Dan box ribbon: { centre x, spacing between boxes, extra gap left of the selection, extra gap right }
function DanSelect:ribbon()
return { 440.6, 135.0, 296.6, 288.4 }
end
function DanSelect:draw_back()
draw_tex("global", "bg", {})
draw_tex("global", "bg_header", {})
draw_tex("global", "bg_footer", {})
draw_tex("global", "footer", {})
end
function DanSelect:draw_cursor(pos, now, since_move)
if since_move < 0 or since_move >= 967 then return true end
local p = since_move / 967
local drift = 30 * p
local fade = 0.984 * (1 - p)
draw_tex("global", "arrow", { index = 0, x = -drift, fade = fade })
draw_tex("global", "arrow", { index = 1, x = drift, fade = fade, mirror = "horizontal" })
return true
end
function DanSelect:draw_title()
draw_tex("global", "dan_select", {})
end
local CONFIRM_OPTION, CONFIRM_YES = 0, 1
-- fade: confirm_fade attribute; index: 0 option / 1 yes / 2 no
function DanSelect:draw_confirm(f, index)
if f <= 0 then return end
tex.draw_rect(0, 0, tex.get_screen_width(), tex.get_screen_height(), 0, 0, 0, math.min(0.5, f) * 255)
draw_tex("confirm_box", "bg", { fade = f })
draw_tex("confirm_box", "confirmation_text", { fade = f })
for i = 0, 1 do
draw_tex("confirm_box", "selection_box", { fade = f, index = i })
end
if index ~= CONFIRM_OPTION then
-- selection_box slots are screen-left / screen-right; yes sits right of no unless swapped
local yes_info = tex.get_texture_info("confirm_box", "yes")
local no_info = tex.get_texture_info("confirm_box", "no")
local yes_right = yes_info and no_info and yes_info.x[1] > no_info.x[1]
local side = ((index == CONFIRM_YES) == (yes_right and true or false)) and 1 or 0
draw_tex("confirm_box", "selection_box_highlight", { fade = f, index = side })
draw_tex("confirm_box", "selection_box_outline", { fade = f, index = side })
end
draw_tex("confirm_box", "yes", { fade = f })
draw_tex("confirm_box", "no", { fade = f })
if T("confirm_box", "option") then
if index == CONFIRM_OPTION then draw_tex("confirm_box", "option_highlight", { fade = f }) end
draw_tex("confirm_box", "option", { fade = f })
end
end
-- Draw a texture at an absolute position, ignoring its own x[0]/y[0] anchor (draw_abs).
local function draw_abs(subset, name, X, Y, fade, frame)
local id = T(subset, name)
if not id then return end
tex.draw_texture(id, {
frame = frame or 0,
x = X - tex_x0(subset, name),
y = Y - tex_y0(subset, name),
fade = fade,
})
end
local BACK_CX, BACK_BOTTOM, BACK_HALF0, BACK_HALF1 = 642, 539, 44.4, 191
local BACK_TOP0, BACK_TOP1, BACK_GROW_MS = 338 - 228, 338 - 231, 150
function DanSelect:draw_back_box(box)
local bx = box:box_x()
local fade = box.fade.attribute
tex.draw_texture(T("back", "folder"), { x = bx, fade = fade })
if box:draw_state() == "closed" then
self.back_open_at = nil
tex.draw_texture(T("back", "chip"), { x = bx, fade = fade })
return
end
self.back_open_at = self.back_open_at or self.now
local ms = self.now - self.back_open_at
local u = math.min(math.max(ms / BACK_GROW_MS, 0), 1)
local half = BACK_HALF0 + (BACK_HALF1 - BACK_HALF0) * u
local top = BACK_TOP0 + (BACK_TOP1 - BACK_TOP0) * u
local l, r = BACK_CX - half, BACK_CX + half - 32
local w, h = r - l - 32, BACK_BOTTOM - top - 32
local function piece(name, x, y, x2, y2)
tex.draw_texture(T("back", name), { x = x, y = y, x2 = x2 and x2 - 32 or 0, y2 = y2 and y2 - 32 or 0 })
end
piece("board_tl", l, top); piece("board_t", l + 32, top, w)
piece("board_tr", r, top); piece("board_r", r, top + 32, nil, h)
piece("board_br", r, BACK_BOTTOM); piece("board_b", l + 32, BACK_BOTTOM, w)
piece("board_bl", l, BACK_BOTTOM); piece("board_l", l, top + 32, nil, h)
piece("board_c", l + 32, top + 32, w, h)
local f = math.min(math.max((ms - 33) / 117, 0), 1)
for _, name in ipairs({ "graphic", "text", "pill" }) do tex.draw_texture(T("back", name), { fade = f }) end
end
function DanSelect:draw_box(box)
if box.is_back then return self:draw_back_box(box) end
track_dan_yellow_box(box)
local state = box:draw_state()
if state == "closed" then
self:draw_dan_closed(box)
else
self:draw_dan_open(box)
end
end
-- ---------------------------------------------------------------------------
-- draw_chip (closed): folder board + vertical chip name.
-- ---------------------------------------------------------------------------
function DanSelect:draw_dan_closed(box)
local bx = box:box_x()
local fade = box.fade.attribute
tex.draw_texture(T("box", "folder"), { frame = box.dan_color, x = bx, fade = fade })
if box.clear_mark > 0 and T("box", "clear_mark") then
tex.draw_texture(T("box", "clear_mark"), { frame = box.clear_mark - 1, x = bx, fade = fade })
end
local name = box:chip_name()
if name then
local nb = cfg("dan_select_chip_name") or cfg("song_select_box_name") or { x = 0, y = 0, height = 0 }
name:draw({
x = bx + nb.x - name.width / 2,
y = nb.y,
y2 = math.min(name.height, nb.height or name.height) - name.height,
fade = fade,
})
end
end
-- ---------------------------------------------------------------------------
-- draw_open: chip + song ribbon + total notes + rank + exam box.
-- ---------------------------------------------------------------------------
function DanSelect:draw_dan_open(box)
self:draw_dan_closed(box)
local f = box.open_fade.attribute
-- The yellow box board the course details sit on (opaque; its growth is driven by
-- the diff-select animations, matching the song-select wheel).
draw_dan_yellow_box(true, 1.0)
local off = SKIN.dan_yellow_box_offset or { x = 0, y = 0 }
local offset_x = off.x or 0
local offset_y = off.y or 0
local n = box:song_count()
for i = 0, n - 1 do
local x = i * offset_x
local y = i * offset_y
tex.draw_texture(T("yellow_box", "genre_banner"), { frame = box:song_genre(i), x = x, y = y, fade = f })
local song_label = T("yellow_box", "song_label")
if song_label then
tex.draw_texture(song_label, { frame = math.min(i, 2), x = x, y = y, fade = f })
end
tex.draw_texture(T("yellow_box", "difficulty"), { frame = box:song_difficulty(i), x = x, y = y, fade = f })
if box:song_difficulty(i) == 4 and T("yellow_box", "difficulty_ura") then
tex.draw_texture(T("yellow_box", "difficulty_ura"), { x = x, y = y, fade = f })
end
tex.draw_texture(T("yellow_box", "difficulty_x"), { x = x, y = y, fade = f })
tex.draw_texture(T("yellow_box", "difficulty_star"), { x = x, y = y, fade = f })
-- Level counter
local lvl = tostring(box:song_level(i))
local margin = (SKIN.dan_level_counter_margin or { x = 0 }).x
local total_w = #lvl * margin
for j = 1, #lvl do
local digit = string.byte(lvl, j) - 48
tex.draw_texture(T("yellow_box", "difficulty_num"), {
frame = digit, x = x - total_w / 2 + (j - 1) * margin, y = y, fade = f })
end
-- Song title + subtitle
local title_text = box:song_title_text(i)
local sub_text = box:song_subtitle_text(i)
local td = cfg("dan_select_title") or { x = 0, y = 0, height = 0 }
local sd = cfg("dan_select_subtitle") or { x = 0, y = 0, height = 0 }
if box:song_masked(i) and T("yellow_box", "hidden_title") then
tex.draw_texture(T("yellow_box", "hidden_title"), { x = x, y = y, fade = f })
elseif title_text then
title_text:draw({
x = td.x + x, y = td.y + y,
y2 = math.min(title_text.height, td.height or title_text.height) - title_text.height,
fade = f })
end
if sub_text then
sub_text:draw({
x = sd.x + x,
y = sd.y + y - math.min(sub_text.height, sd.height or sub_text.height),
y2 = math.min(sub_text.height, sd.height or sub_text.height) - sub_text.height,
fade = f })
end
end
if box.clear_mark > 0 and T("yellow_box", "clear_mark") then
tex.draw_texture(T("yellow_box", "clear_mark"), { frame = box.clear_mark - 1, fade = f })
end
-- Total notes
tex.draw_texture(T("yellow_box", "total_notes_bg"), { fade = f })
tex.draw_texture(T("yellow_box", "total_notes"), { fade = f })
local tn = tostring(box.total_notes)
local tn_margin = (SKIN.total_notes_counter_margin or { x = 0 }).x
for i = 1, #tn do
local digit = string.byte(tn, i) - 48
-- Lumen centres the counter on its 4-digit width
tex.draw_texture(T("yellow_box", "total_notes_counter"), { frame = digit, x = (i - 1 + (4 - #tn) / 2) * tn_margin, fade = f })
end
-- Rank plate / frame
tex.draw_texture(T("yellow_box", "frame"), { frame = box.dan_color, fade = f })
local hori = box:hori_name()
if hori then
local hn = SKIN.dan_hori_name or { x = 0, y = 0, width = 0 }
hori:draw({
x = hn.x - hori.width / 2,
y = hn.y,
x2 = math.min(hori.width, hn.width or hori.width) - hori.width,
fade = f })
end
self:draw_exam_box(box, f)
end
-- ---------------------------------------------------------------------------
-- Exam board (the default "condition rows" layout).
-- ---------------------------------------------------------------------------
function DanSelect:draw_exam_box(box, f)
local offset_y = (cfg("dan_select_exam_info") or { y = 0 }).y
local margin = (SKIN.exam_counter_margin or { x = 0 }).x
-- Four or more conditions do not fit the board: drop the soul-gauge row.
local exam_count = box:exam_count()
local shown = {}
for i = 0, exam_count - 1 do
if not (exam_count >= 4 and box:exam_type(i) == "gauge") then
shown[#shown + 1] = i
end
end
if #shown > 3 then
while #shown > 3 do shown[#shown] = nil end
end
local L = ({ { dt = 38, row0 = 90, pitch = 0 }, { dt = 18, row0 = 37, pitch = 106 } })[#shown]
or { dt = 0, row0 = 0, pitch = offset_y }
local dt = L.dt
local function part(name, p)
p.fade = f
tex.draw_texture(T("yellow_box", name), p)
end
part("exam_box_bottom_right", { y = -dt })
part("exam_box_bottom_left", { y = -dt })
part("exam_box_top_right", { y = dt })
part("exam_box_top_left", { y = dt })
part("exam_box_bottom", { y = -dt })
part("exam_box_right", { y = dt, y2 = -2 * dt })
part("exam_box_left", { y = dt, y2 = -2 * dt })
part("exam_box_top", { y = dt })
part("exam_box_center", { y = dt, y2 = -2 * dt })
part("exam_header", { y = dt })
for idx = 1, #shown do
local i = shown[idx]
local y = L.row0 + (idx - 1) * L.pitch
local etype = box:exam_type(i)
local range = box:exam_range(i)
tex.draw_texture(T("yellow_box", "judge_box"), { y = y, fade = f })
local icon_name = exam_icon_name(etype)
if icon_name then tex.draw_texture(T("yellow_box", icon_name), { y = y, fade = f }) end
-- Centre the whole value (digits, %, suffix) on dan_exam_value_center.x.
local vc = SKIN.dan_exam_value_center or { x = 0, width = 0 }
local digits = tostring(box:exam_red(i))
local pct = (etype == "gauge") and T("yellow_box", "exam_percent") or nil
local suf = (range == "more") and T("yellow_box", "exam_more")
or (range == "less") and T("yellow_box", "exam_less") or nil
local digits_w = (#digits - 1) * margin + tex_x2("yellow_box", "judge_num")
local pct_w = pct and tex_x2("yellow_box", "exam_percent") or 0
local suf_w = suf and tex_x2("yellow_box", (range == "more") and "exam_more" or "exam_less") or 0
local gap = vc.width or 0
local parts = 1 + (pct and 1 or 0) + (suf and 1 or 0)
local total = digits_w + pct_w + suf_w + gap * (parts - 1)
local cx = (vc.x or 0) - total / 2
for j = 1, #digits do
local digit = string.byte(digits, j) - 48
tex.draw_texture(T("yellow_box", "judge_num"),
{ frame = digit, x = cx + (j - 1) * margin - tex_x0("yellow_box", "judge_num"), y = y, fade = f })
end
cx = cx + digits_w + gap
if pct then
tex.draw_texture(pct, { x = cx - tex_x0("yellow_box", "exam_percent"), y = y, fade = f })
cx = cx + pct_w + gap
end
if suf then
local suf_name = (range == "more") and "exam_more" or "exam_less"
tex.draw_texture(suf, { x = cx - tex_x0("yellow_box", suf_name), y = y, fade = f })
end
end
end
+4 -1
View File
@@ -14,6 +14,9 @@ EntryBox = {}
EntryBox.__index = EntryBox
local SS = tex.get_screen_scale()
local SKIN = {
entry_box_text_offset = { y = 29 },
}
-- juu-yon's outline widths look too heavy at this skin's 960 px (user, 2026-09-27): every text
-- outline is drawn at OUTLINE_K of the juu-yon value.
local OUTLINE_K = 0.6
@@ -60,7 +63,7 @@ function EntryBox.new(text_str, font_size)
end
end
end
local off = tex.get_skin_config("entry_box_text_offset")
local off = SKIN.entry_box_text_offset
self.text_y_offset = off and off.y or 29
self.start_ms = tex.get_current_ms()
return self
+5
View File
@@ -30,6 +30,11 @@ end
local CHARA_BPM, CHARA_MISC = 118, 8
-- { nameplate x, y, indicator x, y }: parked off-screen, this skin has no entry nameplate
function EntryPlayer:nameplate_indicator_pos()
return { 0, 2000, 0, 2000 }
end
function EntryPlayer.new(side)
local self = setmetatable({}, EntryPlayer)
self.side = side
+99
View File
@@ -0,0 +1,99 @@
-- The combo counter on the lane: the number (stretching on every hit), the "COMBO" label, the
-- silver / gold digit tiers and, from 100, the three glimmers sweeping over the number.
-- The engine's Combo (combo.h) calls update every frame with the player's combo; this draws it.
local SKIN = {
color_tiers = false, -- silver digits from 50 combo
margin = { x = 30, y = 42 },
glimmer = {
{ x = 168.75, y = 157.5 },
{ x = 150, y = 172.5 },
{ x = 187.5, y = 172.5 },
},
}
Combo = {}
Combo.__index = Combo
local TOTAL_TIME = 250
local CYCLE_TIME = TOTAL_TIME * 2
local WHITE = { 255, 255, 255 }
function Combo.new(combo, current_ms)
local self = setmetatable({}, Combo)
self.combo = combo
self.stretch = tex.get_animation(5, true)
self.glimmer_fade = { 1, 1, 1 }
self.glimmer_y = { 0, 0, 0 }
self.start_times = {
current_ms,
current_ms - (2 / 3) * CYCLE_TIME,
current_ms - (4 / 3) * CYCLE_TIME,
}
self.t_counter = tex.get_texture("combo/counter")
self.t_counter_gold = tex.get_texture("combo/counter_gold")
self.t_counter_100 = tex.get_texture("combo/counter_100")
self.t_gleam = tex.get_texture("combo/gleam")
return self
end
function Combo:update(ms, combo)
if combo ~= self.combo then
self.combo = combo
self.stretch:start()
end
self.stretch:update(ms)
for i = 1, 3 do
local elapsed = (ms - self.start_times[i]) % CYCLE_TIME
if elapsed <= TOTAL_TIME then
self.glimmer_y[i] = -math.floor(elapsed / 16.67)
local fade_start = TOTAL_TIME - 164
self.glimmer_fade[i] = elapsed >= fade_start and 1 - (elapsed - fade_start) / 164 or 1
else
self.glimmer_y[i] = 0
self.glimmer_fade[i] = 0
end
end
end
function Combo:draw(y)
if self.combo < 3 then return end
local lang = tex.language()
if lang ~= self.lang then
self.lang = lang
self.t_combo = tex.get_texture("combo/combo_" .. lang)
self.t_combo_100 = tex.get_texture("combo/combo_100_" .. lang)
end
local str = tostring(self.combo)
local tiers = SKIN.color_tiers
local gold = self.combo >= 100
local silver = tiers and self.combo >= 50 and self.combo < 100
local digits = self.t_counter
if gold then
digits = self.t_counter_gold or self.t_counter_100 or self.t_counter
elseif silver then
digits = self.t_counter_100 or self.t_counter
end
local margin = gold and SKIN.margin.y or SKIN.margin.x
local stretch = self.stretch.attribute
tex.draw_texture(gold and self.t_combo_100 or self.t_combo, { y = y })
for i = 1, #str do
tex.draw_texture(digits, {
frame = str:byte(i) - 48, x = -(#str * margin / 2) + (i - 1) * margin,
y = y - stretch, y2 = stretch,
})
end
if not gold then return end
for j = 1, 3 do
local g = SKIN.glimmer[j]
local color = { WHITE[1], WHITE[2], WHITE[3], 255 * self.glimmer_fade[j] }
for i = 0, #str - 1 do
tex.draw_texture(self.t_gleam, { color = color, x = g.x + i * margin, y = y + g.y + self.glimmer_y[j] })
end
end
end
+78
View File
@@ -0,0 +1,78 @@
local SKIN_S = tex.get_screen_scale()
local SKIN = {
combo_announce_add_x = { x = 409.5 },
combo_announce_thousands_add_x = { x = 265.5 },
combo_announce_number_thousands_x = { x = -48 },
combo_announce_text_offset = { x = -30 * SKIN_S },
combo_announce_thousands_offset = { x = 0 },
combo_announce_hundreds_offset = { x = 0 },
}
-- Combo announce (100/200/.../1000+) using the number+add composite: this skin
-- has no per-digit "announce_digit" strip, only announce_number (hundreds 1-9)
-- and announce_add (thousands digit + "+" marker).
-- Ported from src/objects/game/combo_announce.cpp.
ComboAnnounce = {}
ComboAnnounce.__index = ComboAnnounce
function ComboAnnounce.new(combo, player_num)
local self = setmetatable({}, ComboAnnounce)
local suffix = player_num .. "p"
self.t_bg = tex.get_texture("game/combo/announce_bg_" .. suffix)
self.t_text = tex.get_texture("game/combo/announce_text")
self.t_number = tex.get_texture("game/combo/announce_number")
self.t_add = tex.get_texture("game/combo/announce_add")
self.combo = combo
return self
end
function ComboAnnounce:draw(y, fade_value)
if not self.t_bg then return end
tex.draw_texture(self.t_bg, { y = y, fade = fade_value })
local combo = self.combo
if combo >= 1000 then
local thousands = math.floor(combo / 1000)
local remaining_hundreds = math.floor((combo % 1000) / 100)
local thousands_offset = SKIN.combo_announce_thousands_offset.x
local hundreds_offset = SKIN.combo_announce_hundreds_offset.x
if combo % 1000 == 0 then
if self.t_number then
tex.draw_texture(self.t_number, {
frame = thousands - 1,
x = SKIN.combo_announce_number_thousands_x.x,
y = y, fade = fade_value,
})
end
if self.t_add then
tex.draw_texture(self.t_add, { frame = 0, x = SKIN.combo_announce_add_x.x, y = y, fade = fade_value })
end
else
if thousands <= 5 and self.t_add then
tex.draw_texture(self.t_add, {
frame = thousands,
x = SKIN.combo_announce_thousands_add_x.x + thousands_offset,
y = y, fade = fade_value,
})
end
if remaining_hundreds > 0 and self.t_number then
tex.draw_texture(self.t_number, { frame = remaining_hundreds - 1, x = hundreds_offset, y = y, fade = fade_value })
end
end
if self.t_text then
local text_offset = SKIN.combo_announce_text_offset.x
tex.draw_texture(self.t_text, { x = -text_offset / 2, y = y, fade = fade_value })
end
elseif combo >= 100 then
if self.t_number then
tex.draw_texture(self.t_number, { frame = math.floor(combo / 100) - 1, x = 0, y = y, fade = fade_value })
end
if self.t_text then
tex.draw_texture(self.t_text, { x = 0, y = y, fade = fade_value })
end
end
end
return ComboAnnounce
+214
View File
@@ -0,0 +1,214 @@
local SKIN_S = tex.get_screen_scale()
local SKIN = {
dan_game_hori_name = { x = 115.5, y = 433.5, width = 206.25 },
dan_total_notes_margin = { x = 46 * SKIN_S },
dan_game_exam_border_text = { x = 1232 * SKIN_S, y = 409 * SKIN_S, font_size = math.floor(32 * SKIN_S), outline = 4 },
}
local WHITE, BLACK = { 255, 255, 255, 255 }, { 0, 0, 0, 255 }
local JP_THRESHOLD = {
dan_exam_text_more = "%s 以上",
dan_exam_text_less = "%s 未満",
dan_exam_text_gauge = "%s %以上",
dan_exam_text_gauge_less = "%s %未満",
}
local function exam_threshold_text(exam_type, range, num)
local gauge, less = exam_type == "gauge", range == "less"
local key = gauge and (less and "dan_exam_text_gauge_less" or "dan_exam_text_gauge")
or (less and "dan_exam_text_less" or "dan_exam_text_more")
local fmt = tex.skin_text(key)
if fmt == "" then fmt = JP_THRESHOLD[key] end
fmt = fmt:gsub("%%%%", "\1")
return (fmt:gsub("%%s", tostring(num)):gsub("\1", "%%"))
end
local function pad_for(outline, scale)
return math.floor(outline * scale) + 2
end
-- Lumen row tops (minus the first 3-row top) for 1, 2 and 3 exam rows.
local ROW_OFFSETS = { { 91 }, { 39, 135 }, { 0, 99, 198 } }
DanGameDraw = {}
DanGameDraw.__index = DanGameDraw
function DanGameDraw.new(title)
local self = setmetatable({}, DanGameDraw)
local function T(name) return tex.get_texture("game/dan_info/" .. name) end
-- Labels, thresholds and live values are engine text; only the plates, bars and digits are art.
self.t = {}
for _, n in ipairs({ "exam_bg", "exam_overlay_1", "exam_overlay_2", "exam_fail", "exam_failed",
"total_notes_counter", "frame", "exam_flash" }) do
self.t[n] = T(n)
end
self.bars = { exam_red = T("exam_red"), exam_gold = T("exam_gold"), exam_max = T("exam_max") }
self.captions = {}
self.live = {}
self.fx = {}
local ds = tex.get_skin_config("dan_select_title")
self.hori_name = text.create_raw_text(title, ds and ds.font_size or 40, WHITE, BLACK, false)
return self
end
function DanGameDraw:digit_counter(digits, margin_x, t, index, y, x_offset)
x_offset = x_offset or 0
local n = #digits
for j = 1, n do
tex.draw_texture(t, { frame = digits:byte(j) - 48, x = -(n - j + 1) * margin_x + x_offset,
y = y, index = index })
end
end
function DanGameDraw:caption(s, size, outline)
local key = s .. "\31" .. size .. "\31" .. math.floor(outline * 100)
local c = self.captions[key]
if not c then
c = text.create_raw_text(s, size, WHITE, BLACK, false, outline)
self.captions[key] = c
end
return c
end
-- Engine-drawn threshold phrase ("100 以上"), right-aligned at dan_game_exam_border_text.
local UNIT_CHAR = { hit = "打", renda = "打", score = "点" }
local LIVE_UNIT = { gauge = "%", combo = "コンボ", hit = "打", renda = "打", score = "点" }
-- Live value as engine text number right-aligned at dan_game_exam_value_text.x, unit mark after it.
-- Re-rendered only when the string changes (one cache slot per row part).
function DanGameDraw:live_caption(slot, str, size, outline)
local c = self.live[slot]
if not c or c.str ~= str then
-- first sight is not a change: only later updates pop
c = { str = str, cap = text.create_raw_text(str, size, WHITE, BLACK, false, outline),
changed = c and tex.get_current_ms() or nil }
self.live[slot] = c
end
return c.cap, c.changed
end
-- Lumen digit pop: 1.0 -> 1.15 in 33 ms, back to 1.0 in 100 ms.
local function pop_scale(changed)
if not changed then return 1.0 end
local t = tex.get_current_ms() - changed
if t < 33 then return 1.0 + 0.15 * t / 33 end
if t < 133 then return 1.15 - 0.15 * (t - 33) / 100 end
return 1.0
end
function DanGameDraw:draw_live_text(info, y, slot)
local s = tex.get_screen_scale()
local vt = { x = 490 * s, y = 384 * s }
local ol = 4.0
local size = math.floor(32 * s)
local pad = pad_for(ol, tex.get_screen_scale())
local num, changed = self:live_caption(slot, tostring(info.counter_value), size, ol)
num:draw({ x = vt.x - num.width + pad, y = vt.y - pad + y, scale = pop_scale(changed), center = true })
local unit = LIVE_UNIT[info.exam_type]
if unit then
local u = self:live_caption(slot + 10, unit, size, ol)
u:draw({ x = vt.x + 4 - pad, y = vt.y - pad + y })
end
end
function DanGameDraw:border_caption(info)
local bt = SKIN.dan_game_exam_border_text
local ol = bt.outline >= 0 and bt.outline or 4.0 / tex.get_screen_scale()
local unit = UNIT_CHAR[info.exam_type] or ""
local cap = self:caption(exam_threshold_text(info.exam_type, info.exam_range, info.red_value .. unit),
bt.font_size > 0 and bt.font_size or 36, ol)
local pad = pad_for(ol, tex.get_screen_scale())
return cap, bt.x - cap.width + pad, bt.y - pad
end
-- Exam type label ("コンボ数") right-aligned just left of the threshold phrase.
function DanGameDraw:draw_label(info, x, y)
local str = tex.skin_text("dan_exam_label_" .. info.exam_type)
if str == "" then return end
local s = tex.get_screen_scale()
local lc = { x = 8 * s, y = 409 * s }
local ol = 4.0
local cap = self:caption(str, math.floor(32 * s), ol)
local pad = pad_for(ol, tex.get_screen_scale())
cap:draw({ x = x - lc.x - cap.width + pad, y = lc.y - pad + y })
end
-- White strip over the bar. On a colour change (red -> gold -> max): alpha 0 -> 0.98 in 83 ms, out over
-- 883 ms. While cleared (max) a slow glow loops: 0 -> 0.35 in 433 ms, out over 450 ms, every 1000 ms.
function DanGameDraw:bar_flash(info, y, slot)
local fx = self.fx[slot]
local now = tex.get_current_ms()
if not fx then
fx = { bar = info.bar_texture, start = nil }
self.fx[slot] = fx
elseif fx.bar ~= info.bar_texture then
fx.bar, fx.start = info.bar_texture, now
end
local a = 0.0
if fx.start then
local t = now - fx.start
if t < 83 then a = 0.98 * t / 83
elseif t < 966 then a = 0.98 * (1.0 - (t - 83) / 883)
else fx.start = nil end
end
if fx.bar == "exam_max" then
local t = now % 1000
local g
if t < 433 then g = 0.352 * (1.0 - (1.0 - t / 433) ^ 2)
elseif t < 883 then g = 0.352 * (1.0 - (t - 433) / 450) ^ 2
else g = 0.0 end
a = math.max(a, g)
end
if a > 0.0 and info.bar_width > 0 then
tex.draw_texture(self.t.exam_flash, { y = y, x2 = info.bar_width, fade = a })
end
end
function DanGameDraw:exam_row(info, y, slot)
local t = self.t
tex.draw_texture(t.exam_bg, { y = y })
tex.draw_texture(t.exam_overlay_1, { y = y })
local bar = self.bars[info.bar_texture]
if info.failed then
if t.exam_fail then tex.draw_texture(t.exam_fail, { y = y, x2 = info.bar_width }) end
elseif bar then
tex.draw_texture(bar, { y = y, x2 = info.bar_width })
end
if not info.failed then self:bar_flash(info, y, slot) end
local cap, cx, cy = self:border_caption(info)
self:draw_label(info, cx, y)
cap:draw({ x = cx, y = cy + y })
tex.draw_texture(t.exam_overlay_2, { y = y })
if info.failed then
tex.draw_texture(t.exam_failed, { y = y })
else
self:draw_live_text(info, y, slot)
end
end
function DanGameDraw:draw(s)
local t = self.t
local tn = tex.get_skin_config("dan_game_total_notes_text")
local str = tex.skin_text("dan_game_total_notes_text")
if str ~= "" then
local ol = tn.outline >= 0 and tn.outline or 4.0 / tex.get_screen_scale()
local cap = self:caption(str, tn.font_size > 0 and tn.font_size or 36, ol)
cap:draw({ x = tn.x - cap.width / 2.0, y = tn.y, x2 = math.min(cap.width, tn.width) - cap.width })
end
self:digit_counter(tostring(s.remaining_notes), SKIN.dan_total_notes_margin.x,
t.total_notes_counter, 0, 0)
local rows = {}
for _, info in ipairs(s.rows) do
if info.exam_type ~= "gauge" and #rows < 3 then rows[#rows + 1] = info end
end
for slot, info in ipairs(rows) do
self:exam_row(info, ROW_OFFSETS[#rows][slot], slot)
end
tex.draw_texture(t.frame, { frame = s.dan_color })
local hn = SKIN.dan_game_hori_name
local h = self.hori_name
h:draw({ x = hn.x - h.width / 2.0, y = hn.y, x2 = math.min(h.width, hn.width) - h.width })
end
+56
View File
@@ -0,0 +1,56 @@
-- The judge counter shown beside the lane when it is enabled in the settings: the good / ok / bad
-- percentages and counts, the total percentage and the drum roll hits.
-- The engine's JudgeCounter (judge_counter.h) keeps the counts; this draws them.
local SKIN = {
margin = 20.175,
x = { 228.15, 157.95, 236.925 },
y = { 453.375, 490.29, 528.18, 411.48 },
drumroll_y = 590.04,
}
JudgeCounter = {}
JudgeCounter.__index = JudgeCounter
local ORANGE, WHITE = { 253, 161, 0, 255 }, { 255, 255, 255, 255 }
function JudgeCounter.new()
local self = setmetatable({}, JudgeCounter)
local function T(name) return tex.get_texture("judge_counter/" .. name) end
self.counter, self.bg, self.total_percent = T("counter"), T("bg"), T("total_percent")
self.judgments, self.drumrolls, self.percent = T("judgments"), T("drumrolls"), T("percent")
return self
end
-- right-aligned digits ending at x
function JudgeCounter:draw_counter(value, x, y, color)
if not (value >= 0) then value = 0 end -- guards negative values and NaN
local str = tostring(math.floor(value + 0.5))
for i = 1, #str do
tex.draw_texture(self.counter, {
color = color, frame = str:byte(i) - 48,
x = x - (#str - (i - 1)) * SKIN.margin, y = y,
})
end
end
function JudgeCounter:draw(good, ok, bad, drumrolls)
tex.draw_texture(self.bg)
tex.draw_texture(self.total_percent)
tex.draw_texture(self.judgments)
tex.draw_texture(self.drumrolls)
for i = 0, 3 do tex.draw_texture(self.percent, { color = ORANGE, index = i }) end
local total = good + ok + bad
if total == 0 then total = 1 end
local x, y = SKIN.x, SKIN.y
self:draw_counter(good / total * 100, x[1], y[1], ORANGE)
self:draw_counter(ok / total * 100, x[1], y[2], ORANGE)
self:draw_counter(bad / total * 100, x[1], y[3], ORANGE)
self:draw_counter((good + ok) / total * 100, x[3], y[4], ORANGE)
self:draw_counter(good, x[2], y[1], WHITE)
self:draw_counter(ok, x[2], y[2], WHITE)
self:draw_counter(bad, x[2], y[3], WHITE)
self:draw_counter(drumrolls, x[2], SKIN.drumroll_y, WHITE)
end
+104
View File
@@ -0,0 +1,104 @@
local SKIN_S = tex.get_screen_scale()
local SKIN = {
note_arc_start_x_offset = { x = 150 },
note_arc_curve_height = { height = 244.5 },
note_arc_balloon_p2_y = { y = 435 * SKIN_S },
}
NoteArc = {}
NoteArc.__index = NoteArc
local ARC_POINTS = 100
local FRAME_MS = 16.67
local function trunc(v) return v < 0 and math.ceil(v) or math.floor(v) end
function NoteArc.new(note_tex_type, big, is_balloon, player_num, start_x, start_y)
local self = setmetatable({}, NoteArc)
self.big = big
self.is_balloon = is_balloon
self.p2 = player_num == 2
local curve_height = SKIN.note_arc_curve_height.height
local sx = start_x + SKIN.note_arc_start_x_offset.x
local sy = start_y + tex.get_skin_config("game_notes").y
local hit = tex.get_skin_config("game_gauge_hit_effect_note")
local ex, ey = hit.x, hit.y
if self.p2 then ey = ey + hit.height end
self.sx, self.sy, self.ex, self.ey = sx, sy, ex, ey
self.cx = (sx + ex) / 2
if self.p2 then
self.cy = math.max(sy, ey) + curve_height -- arc downward
else
self.cy = math.min(sy, ey) - curve_height -- arc upward
end
self.t_note = tex.get_texture("game/notes/" .. note_tex_type)
self.note_width = tex.get_texture_info("notes", tostring(note_tex_type)).width
self.t_firework = big and tex.get_texture("game/hit_effect/firework") or nil
self.firework_frames = self.t_firework and tex.get_texture_info("hit_effect", "firework").frame_count or 0
self.t_rainbow = tex.get_texture("game/balloon/rainbow")
if self.t_rainbow then
self.rainbow_info = tex.get_texture_info("balloon", "rainbow")
end
return self
end
-- bezier sampled at the same 100 steps the C++ cache used, truncated to whole pixels
function NoteArc:point(progress)
local t = math.min(math.floor(progress * ARC_POINTS), ARC_POINTS) / ARC_POINTS
local ti = 1 - t
return trunc(ti * ti * self.sx + 2 * ti * t * self.cx + t * t * self.ex),
trunc(ti * ti * self.sy + 2 * ti * t * self.cy + t * t * self.ey)
end
function NoteArc:draw_rainbow(y, raw_progress)
local w, h = self.rainbow_info.width, self.rainbow_info.height
local trail_start = math.max(0, raw_progress - 0.5)
local trail_end = math.min(raw_progress, 1)
if trail_end <= trail_start then return end
local crop_start = math.floor(trail_start * w + 0.5)
local crop_w = math.floor(trail_end * w + 0.5) - crop_start
if crop_w <= 0 then return end
local y_pos = self.p2 and SKIN.note_arc_balloon_p2_y.y or 0
tex.draw_texture(self.t_rainbow, {
x = crop_start, y = y + y_pos, x2 = -w + crop_w,
src = { x = crop_start, y = 0, width = crop_w, height = self.p2 and -h or h },
})
end
function NoteArc:draw_fireworks(y, elapsed_ms, duration_ms)
local half = self.note_width * 0.6 / 2
for i = 0, 8 do
local spawn_ms = (3 + 3 * i) * FRAME_MS
local age_ms = elapsed_ms - spawn_ms
local frame = math.floor(age_ms / FRAME_MS)
if age_ms >= 0 and frame < self.firework_frames then
local fx, fy = self:point(math.min(spawn_ms / duration_ms, 1))
tex.draw_texture(self.t_firework, { frame = frame, scale = 0.8, x = fx - half, y = y + fy - half, blend = "additive" })
end
end
end
function NoteArc:draw(y, elapsed_ms, duration_ms)
local progress = math.min(1, elapsed_ms / duration_ms)
local x_i, y_i = self:point(progress)
if self.is_balloon then
if not self.t_rainbow then
if progress < 1 then tex.draw_texture(self.t_note, { x = x_i, y = y + y_i }) end
return
end
self:draw_rainbow(y, elapsed_ms / duration_ms)
elseif self.big and self.t_firework and self.firework_frames > 0 then
self:draw_fireworks(y, elapsed_ms, duration_ms)
end
if progress < 1 then
tex.draw_texture(self.t_note, { x = x_i, y = y + y_i })
end
end
return NoteArc
+165
View File
@@ -0,0 +1,165 @@
-- Practice mode pause menu: the row of bars, and the confirm dialogs (end / another song / restart /
-- auto play). The engine's PracticeMenu keeps the state (open, selected row, dialog, selected button)
-- and the actions; this draws it and says which bar or button a pointer is over.
--
-- Rows, in order: end game, another song, restart, auto play, jump to mark, set mark, close.
PracticeMenu = {}
PracticeMenu.__index = PracticeMenu
local S = tex.get_screen_scale()
local SKIN = {
panel = { y = 188.0001, width = 940 * S, height = 460 * S },
bar = { gap = 36 * S, width = 92 * S, height = 378 * S },
label = { y = 16 * S, width = 350 * S, font_size = 32 },
dialog_font_size = 32,
dialog_title_y = 100 * S,
dialog_title_auto_y = 56 * S,
button = { x = 160 * S, y = 240 * S, width = 212 * S, height = 102 * S },
auto_label = { y = 150 * S, width = 290 * S },
auto_toggle = { gap = 40 * S, y = 280 * S, width = 240 * S },
toggle_y = 6 * S,
}
local WHITE, BLACK = { 255, 255, 255, 255 }, { 0, 0, 0, 255 }
local ROWS = {
"practice_menu_end_game", "practice_menu_another_song", "practice_menu_restart",
"practice_menu_auto_play", "practice_menu_jump_to_mark", "practice_menu_set_mark",
"practice_menu_close",
}
local DIALOGS = {
auto = { title = "practice_menu_confirm_auto", left = "practice_menu_auto_on", right = "practice_menu_auto_off" },
restart = { title = "practice_menu_confirm_restart", left = "practice_menu_yes", right = "practice_menu_no" },
another = { title = "practice_menu_confirm_another", left = "practice_menu_yes", right = "practice_menu_no" },
["end"] = { title = "practice_menu_confirm_end", left = "practice_menu_yes", right = "practice_menu_no" },
}
local function sprite(name)
return tex.get_texture("practice/" .. name)
end
function PracticeMenu.new()
return setmetatable({ lang = nil }, PracticeMenu)
end
-- the texts, rebuilt when the language changes
function PracticeMenu:texts()
local lang = tex.language()
if self.lang == lang then return end
self.lang = lang
self.rows, self.dialogs = {}, {}
local base = SKIN.label.font_size > 0 and SKIN.label.font_size or 1
for i, key in ipairs(ROWS) do
local label = tex.skin_text(key)
local _, chars = label:gsub("[^\128-\191]", "")
local fs, v = base, 1.0
if chars > 1 then
v = (SKIN.label.width / fs - 1.0) / (chars - 1)
if v < 0.7 then
v = 0.7
fs = math.max(1, math.floor(SKIN.label.width / (1.0 + (chars - 1) * 0.7)))
elseif v > 1.0 then
v = 1.0
end
end
self.rows[i] = text.create_raw_text(label, fs, WHITE, BLACK, true, 5.0 * fs / base, 2.0, v)
end
local fs = SKIN.dialog_font_size > 0 and SKIN.dialog_font_size or 1
for name, d in pairs(DIALOGS) do
self.dialogs[name] = {
title = text.create_raw_text(tex.skin_text(d.title), fs, WHITE, BLACK, false),
left = text.create_raw_text(tex.skin_text(d.left), fs, WHITE, BLACK, false),
right = text.create_raw_text(tex.skin_text(d.right), fs, WHITE, BLACK, false),
}
end
end
local function panel_x()
return (tex.get_screen_width() - SKIN.panel.width) / 2.0
end
-- x of the left and right dialog buttons, and their y
local function dialog_buttons(dialog)
local px, b = panel_x(), SKIN.button
if dialog == "auto" then
local t = SKIN.auto_toggle
local total = b.width * 2 + t.width + t.gap * 2
local left = px + (SKIN.panel.width - total) / 2.0
return left, left + b.width + t.gap + t.width + t.gap, SKIN.panel.y + t.y
end
return px + b.x, px + SKIN.panel.width - b.x - b.width, SKIN.panel.y + b.y
end
local function inside(x, y, left, top, w, h)
return x >= left and x <= left + w and y >= top and y <= top + h
end
-- bar under the pointer (0-based), or the dialog button (0 = left, 1 = right); nil if none
function PracticeMenu:hit_test(x, y, dialog)
local b = SKIN.button
if dialog then
local left, right, row_y = dialog_buttons(dialog)
if inside(x, y, left, row_y, b.width, b.height) then return 0 end
if inside(x, y, right, row_y, b.width, b.height) then return 1 end
return nil
end
local n, bar = #ROWS, SKIN.bar
local pad = (SKIN.panel.width - n * bar.width - (n - 1) * bar.gap) / 2.0
local top = SKIN.panel.y + (SKIN.panel.height - bar.height) / 2.0
for i = 0, n - 1 do
if inside(x, y, panel_x() + pad + i * (bar.width + bar.gap), top, bar.width, bar.height) then
return i
end
end
return nil
end
function PracticeMenu:draw(index)
self:texts()
local n, bar, px = #ROWS, SKIN.bar, panel_x()
local pad = (SKIN.panel.width - n * bar.width - (n - 1) * bar.gap) / 2.0
tex.draw_texture(sprite("menu_panel"), { x = px, y = SKIN.panel.y })
for i = 0, n - 1 do
local x = px + pad + i * (bar.width + bar.gap)
local y = SKIN.panel.y + (SKIN.panel.height - bar.height) / 2.0
tex.draw_texture(sprite(i == index and "menu_bar_selected" or "menu_bar"), { x = x, y = y })
local label = self.rows[i + 1]
-- top-aligned like the arcade, not centred
label:draw({ x = x + (bar.width - label.width) / 2.0, y = y + SKIN.label.y })
end
end
-- sel: 0 = left button, 1 = right
function PracticeMenu:draw_dialog(dialog, sel)
self:texts()
local px, pw, b = panel_x(), SKIN.panel.width, SKIN.button
local d = self.dialogs[dialog]
if not d then return end
tex.draw_texture(sprite("menu_panel"), { x = px, y = SKIN.panel.y })
-- the auto dialog stacks a label chip and the toggle row below its title, so the title sits
-- high; the yes/no confirms sit lower
local title_y = (dialog == "auto") and SKIN.dialog_title_auto_y or SKIN.dialog_title_y
d.title:draw({ x = px + (pw - d.title.width) / 2.0, y = SKIN.panel.y + title_y })
local left, right, row_y = dialog_buttons(dialog)
if dialog == "auto" then
tex.draw_texture(sprite("menu_auto_label"),
{ x = px + (pw - SKIN.auto_label.width) / 2.0, y = SKIN.panel.y + SKIN.auto_label.y })
-- the red knob slides toward whichever side is picked
tex.draw_texture(sprite("menu_toggle"), {
mirror = sel == 1 and "horizontal" or nil,
x = left + b.width + SKIN.auto_toggle.gap, y = row_y + SKIN.toggle_y,
})
end
tex.draw_texture(sprite(sel == 0 and "menu_button_selected" or "menu_button"), { x = left, y = row_y })
tex.draw_texture(sprite(sel == 1 and "menu_button_selected" or "menu_button"), { x = right, y = row_y })
d.left:draw({ x = left + (b.width - d.left.width) / 2.0, y = row_y + (b.height - d.left.height) / 2.0 })
d.right:draw({ x = right + (b.width - d.right.width) / 2.0, y = row_y + (b.height - d.right.height) / 2.0 })
end
+115
View File
@@ -0,0 +1,115 @@
-- The score on the lane cover, and the "+100 / +300 ..." numbers that pop up before they are added
-- to it. The engine's ScoreCounter (score_counter.h) calls update every frame with the player's
-- score and add_popup whenever points are earned; this owns the animations and draws them.
local SKIN = {
delay_score_addition = true, -- the counter catches up when a popup finishes, not at once
margin = 30,
pos = { x = 913.5, y = -16.5 },
popup_p2_offset = 382.5,
}
ScoreCounter = {}
ScoreCounter.__index = ScoreCounter
local MAX_POPUPS = 5 -- add_popup(value, true) drops a popup past this many
local POPUP_COLOR = { { 254, 102, 0 }, { 84, 250, 238 } } -- by player
local function digit(ch)
return (ch >= 48 and ch <= 57) and ch - 48 or 0
end
function ScoreCounter.new(is_2p, player_num)
local self = setmetatable({}, ScoreCounter)
self.is_2p = is_2p
self.player_num = player_num
self.score = 0
self.stretch = tex.get_animation(4, true)
self.t_cover = tex.get_texture("lane/lane_score_cover")
self.t_number = tex.get_texture("lane/score_number")
self.popups = {}
return self
end
function ScoreCounter:set_count(score)
if score ~= self.score then
self.score = score
self.stretch:start()
end
end
function ScoreCounter:add_popup(value, limited)
if limited and #self.popups >= MAX_POPUPS then return end
local anim = function(id)
local a = tex.get_animation(id, true)
a:start()
return a
end
local str = tostring(math.max(math.floor(value), 0))
local base = POPUP_COLOR[self.player_num] or POPUP_COLOR[1]
self.popups[#self.popups + 1] = {
str = str, base = base, alpha = 255, y_list = {},
fade_1 = anim(35), move_x = anim(36), fade_2 = anim(37), move_y_pre = anim(38), move_y_fan = anim(39),
}
end
-- score: the player's current score. With delay_score_addition the counter only catches up
-- once a popup has finished, otherwise it follows at once.
function ScoreCounter:update(ms, score)
local delay = SKIN.delay_score_addition
local i = 1
while i <= #self.popups do
local p = self.popups[i]
p.fade_1:update(ms)
p.move_x:update(ms)
p.move_y_pre:update(ms)
p.move_y_fan:update(ms)
p.fade_2:update(ms)
local fade = p.fade_1.is_finished and p.fade_2.attribute or p.fade_1.attribute
p.alpha = 255 * fade
for k = 1, #p.str do p.y_list[k] = p.move_y_fan.attribute + (k - 1) * 5 end
if p.fade_2.is_finished then
table.remove(self.popups, i)
if delay then self:set_count(score) end
else
i = i + 1
end
end
if not delay then self:set_count(score) end
self.stretch:update(ms)
end
function ScoreCounter:draw(y)
local p2_offset = self.is_2p and tex.get_skin_config("game_score_counter_2p_y_offset").y or 0
if self.is_2p then
tex.draw_texture(self.t_cover, { mirror = "vertical", y = y + p2_offset })
else
tex.draw_texture(self.t_cover, { y = y })
end
local str = tostring(math.max(self.score, 0))
local margin, stretch = SKIN.margin, self.stretch.attribute
local start_x = SKIN.pos.x - #str * margin
local y_pos = y + SKIN.pos.y + p2_offset
for i = 1, #str do
tex.draw_texture(self.t_number, {
frame = str:byte(i) - 48, x = start_x + (i - 1) * margin, y = y_pos - stretch, y2 = stretch,
})
end
local direction = self.is_2p and -1 or 1
for _, p in ipairs(self.popups) do
local x = p.move_x.attribute
if math.abs(x) >= 1e-6 then
local px = x - #p.str * margin
local color = { p.base[1], p.base[2], p.base[3], p.alpha }
for i = 1, #p.str do
local y_i = p.move_y_pre.is_finished and p.y_list[i] or p.move_y_pre.attribute
tex.draw_texture(self.t_number, {
color = color, frame = digit(p.str:byte(i)), x = px + (i - 1) * margin,
y = y_i * direction + y + (self.is_2p and SKIN.popup_p2_offset or 0),
})
end
end
end
end
+31
View File
@@ -0,0 +1,31 @@
EntryOverlay = {}
EntryOverlay.__index = EntryOverlay
function EntryOverlay.new(online)
local self = setmetatable({}, EntryOverlay)
self.online = online
self.banapass_or = tex.get_texture("global/overlay/banapass_or")
self.banapass_card = tex.get_texture("global/overlay/banapass_card")
self.banapass_osaifu = tex.get_texture("global/overlay/banapass_osaifu_keitai")
self.banapass_no = tex.get_texture("global/overlay/banapass_no")
self.camera = tex.get_texture("global/overlay/camera")
return self
end
function EntryOverlay:update(current_ms, online)
self.online = online
end
function EntryOverlay:draw(is_title)
local s = tex.get_screen_scale()
local x = 0
local y = -2000
local frame = self.online and 1 or 0
tex.draw_texture(self.banapass_or, { frame = frame, x = x, y = y })
tex.draw_texture(self.banapass_card, { frame = frame, x = x, y = y })
tex.draw_texture(self.banapass_osaifu, { frame = frame, x = x, y = y })
if not self.online then
tex.draw_texture(self.banapass_no, { frame = frame, x = x, y = y })
end
tex.draw_texture(self.camera, { frame = frame, x = x, y = y })
end
+104
View File
@@ -0,0 +1,104 @@
local SKIN = {
indicator_face_offset = { x = 157.5 },
indicator_kat_offset = { x = 20.7 },
indicator_don_offset = { x = 192.6 },
}
Indicator = {}
Indicator.__index = Indicator
Indicator.State = {
SKIP = 0,
SIDE = 1,
SELECT = 2,
WAIT = 3,
CANCEL = 4,
}
local TEXT_KEYS = {
[Indicator.State.SKIP] = "indicator_text_skip",
[Indicator.State.SIDE] = "indicator_text_skip",
[Indicator.State.SELECT] = "indicator_text",
[Indicator.State.WAIT] = "indicator_text_wait",
[Indicator.State.CANCEL] = "indicator_text_cancel",
}
function Indicator.new(state)
local self = setmetatable({}, Indicator)
self.state = state
self.background = tex.get_texture("global/indicator/background")
self.drum_face = tex.get_texture("global/indicator/drum_face")
self.drum_kat = tex.get_texture("global/indicator/drum_kat")
self.drum_don = tex.get_texture("global/indicator/drum_don")
self.blue_arrow = tex.get_texture("global/indicator/blue_arrow")
self.don_fade = tex.get_animation(6, "global")
self.blue_arrow_move = tex.get_animation(7, "global")
self.blue_arrow_fade = tex.get_animation(8, "global")
self.cancel_fade = tex.get_animation(67, "global")
self.select_text = text.create_text(TEXT_KEYS[state], { 255, 255, 255, 255 }, { 0, 0, 0, 0 }, false, 0, -5)
local text_config = tex.get_skin_config("indicator_text")
local bg_info = tex.get_texture_info("indicator", "background")
self.text_offset_x = text_config.x
self.bg_half_height = bg_info.height / 2
self.kat_offset_x = SKIN.indicator_kat_offset.x
self.face_offset_x = SKIN.indicator_face_offset.x
self.don_offset_x = SKIN.indicator_don_offset.x
return self
end
-- { x, y } of the indicator on a screen
function Indicator:screen_pos(screen)
if screen == "dan_select" then return { 260, 560 } end
return { 0, 2000 }
end
function Indicator:update(current_ms)
self.don_fade:update(current_ms)
self.blue_arrow_move:update(current_ms)
self.blue_arrow_fade:update(current_ms)
self.cancel_fade:update(current_ms)
end
function Indicator:draw(x, y, fade)
fade = fade or 1.0
local state_val = self.state
tex.draw_texture(self.background, { x = x, y = y, fade = fade })
local text_y = y + self.bg_half_height - (self.select_text.height / 2)
self.select_text:draw({ x = x, y = text_y, fade = fade })
if self.state == Indicator.State.WAIT then
return
end
if self.state == Indicator.State.CANCEL then
local kat_fade = math.min(fade, self.cancel_fade.attribute)
tex.draw_texture(self.drum_face, { x = x, y = y, fade = fade, index = Indicator.State.SELECT })
tex.draw_texture(self.drum_kat, { x = x, y = y, fade = kat_fade })
tex.draw_texture(self.drum_kat, { mirror = "horizontal", x = x + self.kat_offset_x, y = y, fade = kat_fade })
return
end
tex.draw_texture(self.drum_face, { x = x, y = y, fade = fade, index = state_val })
if self.state == Indicator.State.SELECT then
local kat_fade = math.min(fade, self.don_fade.attribute)
local arrow_fade = math.min(fade, self.blue_arrow_fade.attribute)
tex.draw_texture(self.drum_kat, { x = x, y = y, fade = kat_fade })
tex.draw_texture(self.drum_kat, { mirror = "horizontal", x = x + self.kat_offset_x, y = y, fade = kat_fade })
tex.draw_texture(self.drum_face, { x = x + self.face_offset_x, y = y, fade = fade, index = state_val })
tex.draw_texture(self.drum_don, { x = x + self.don_offset_x, y = y, fade = kat_fade, index = state_val })
tex.draw_texture(self.blue_arrow, { x = x - self.blue_arrow_move.attribute, y = y, fade = arrow_fade })
tex.draw_texture(self.blue_arrow,
{ mirror = "horizontal", x = x + self.blue_arrow_move.attribute, y = y, fade = arrow_fade, index = 1 })
else
tex.draw_texture(self.drum_don, { x = x, y = y, fade = math.min(fade, self.don_fade.attribute), index = state_val })
end
end
+5 -1
View File
@@ -1,3 +1,7 @@
local SKIN_S = tex.get_screen_scale()
local SKIN = {
timer_text_margin = { x = 44 * SKIN_S },
}
Timer = {}
Timer.__index = Timer
@@ -20,7 +24,7 @@ function Timer.new(time, current_time_ms, confirm_func, is_frozen)
self.highlight_resize = tex.get_animation(10, "global")
self.highlight_fade = tex.get_animation(11, "global")
self.margin = tex.get_skin_config("timer_text_margin").x
self.margin = SKIN.timer_text_margin.x
return self
end
+22
View File
@@ -0,0 +1,22 @@
-- Loading screen countdown: kidou/countdown frames 0-9 are the digits, frame 10 the separator,
-- drawn as S"CC (seconds, hundredths) counting down. `seconds` is how long the screen stays at
-- least (0 = no countdown); x is the centre, y the top, pitch the distance between digits.
local M = { seconds = 10 }
function M.draw(left_ms)
local secs = math.floor(left_ms / 1000)
local hundredths = math.floor(left_ms / 10) % 100
local frames = {}
for c in tostring(secs):gmatch(".") do frames[#frames + 1] = tonumber(c) end
frames[#frames + 1] = 10
frames[#frames + 1] = hundredths // 10
frames[#frames + 1] = hundredths % 10
local sprite = tex.get_texture("loading/kidou/countdown")
local x0 = 480 - 24.75 * #frames / 2
for i, frame in ipairs(frames) do
tex.draw_texture(sprite, { frame = frame, x = x0 + 24.75 * (i - 1), y = 530 })
end
end
return M
+2 -2
View File
@@ -122,7 +122,7 @@ function DanResultDraw.new(rd, prev_arrival, prev_best_score, best_score_show,
self.t_clear_variant = (r >= 4) and self.t_gold_clear or self.t_red_clear
end
local dan_title_cfg = tex.get_skin_config("dan_title") or {}
local dan_title_cfg = tex.get_skin_config("dan_select_title") or {}
local font_size = TITLE_FONT
self.hori_name = text.create_raw_text(rd.dan_title, font_size,
{255,255,255,255}, {0,0,0,255}, false,
@@ -284,7 +284,7 @@ end
function DanResultDraw:draw_exam_info(fade, now)
local rd = self.rd
local on_page = now - self.page_start_ms
local dei = cfg("dan_exam_info")
local dei = cfg("dan_select_exam_info")
local offset_y = dei.y
local margin = dei.x
local border_pitch = margin -- no exam_border_counter in this skin
+3
View File
@@ -277,5 +277,8 @@ function ResultPlayer:draw_title()
end
function ResultPlayer:draw_gauge() end
-- This skin shows no song number / title on the result screen.
function ResultPlayer:draw_song_info() end
function ResultPlayer:chara_pos() return { -2000, -2000, 1.0 } end
function ResultPlayer:nameplate_pos() return { -2000, -2000, 0.0 } end
@@ -0,0 +1,236 @@
local SKIN = {
yellow_box_bottom = { y = 528 },
}
-- Shared BaseBox + YellowBox draw code and text helpers, split out of song_select.lua.
-- Mirrors src/objects/song_select/file_navigator/box_base.cpp and box_yellow.cpp.
-- All positions are in the skin's own pixels (1280x720 for PyTaikoGreen); box positions
-- and engine animations are already in screen pixels.
local M = {}
-- TextureIndex (box/folder_texture colour frame) — must match enums.h.
local TI_NONE, TI_VOCALOID, TI_DEFAULT, TI_RECOMMENDED, TI_FAVORITE, TI_RECENT = 0, 1, 2, 3, 4, 5
-- GenreIndex (raw enum; folder_graphic / diff_overlay frame) — must match enums.h.
local GI_DIFFICULTY = 14
M.TI_NONE, M.TI_DEFAULT = TI_NONE, TI_DEFAULT
M.GI_DIFFICULTY = GI_DIFFICULTY
-- Lazy texture cache: tex.get_texture returns nil on a miss, and the C++ board code
-- dereferences these unconditionally, so cache the resolved id (or nil) once.
local _tex = {}
local function T(subset, name)
local key = subset .. "/" .. name
local id = _tex[key]
if id == nil then
id = tex.get_texture("song_select/" .. subset .. "/" .. name)
_tex[key] = id
end
return id
end
M.T = T
-- Lazy skin_config geometry.
local _song_box_bg_width, _yellow_box_bottom_y, _edge_height
local function song_box_bg_width()
if _song_box_bg_width == nil then
local c = tex.get_skin_config("song_select_box_bg")
_song_box_bg_width = (c and c.width) or 0
end
return _song_box_bg_width
end
local function yellow_box_bottom_y()
if _yellow_box_bottom_y == nil then
local c = SKIN.yellow_box_bottom
_yellow_box_bottom_y = (c and c.y) or 0
end
return _yellow_box_bottom_y
end
local function edge_height()
if _edge_height == nil then
local info = tex.get_texture_info("yellow_box", "yellow_box_bottom_right")
_edge_height = (info and info.height) or 0
end
return _edge_height
end
M.song_box_bg_width = song_box_bg_width
-- Count UTF-8 codepoints (== Lua string length for ASCII; handles multibyte genres).
function M.utf8_char_count(s)
local count = 0
local i = 1
while i <= #s do
local c = s:byte(i)
if not c then break end
if (c & 0xC0) ~= 0x80 then count = count + 1 end
i = i + 1
end
return count
end
-- The five yellow_box shadow strips. index 0 = closed (1p), index 1 = open (2p/highlight).
function M.draw_shadows(bx, by, fade, index)
tex.draw_texture(T("yellow_box", "shadow_bottom_left"), { x = bx, y = by, fade = fade, index = index })
tex.draw_texture(T("yellow_box", "shadow_bottom"), { x = bx, y = by, fade = fade, index = index })
tex.draw_texture(T("yellow_box", "shadow_bottom_right"), { x = bx, y = by, fade = fade, index = index })
tex.draw_texture(T("yellow_box", "shadow_right"), { x = bx, y = by, fade = fade, index = index })
tex.draw_texture(T("yellow_box", "shadow_top_right"), { x = bx, y = by, fade = fade, index = index })
end
-- The 9-slice yellow frame (YellowBox::draw). is_diff picks the diff-select animation set.
function M.draw_yellow_box(is_diff, fade, y_offset)
y_offset = y_offset or 0
local right_x = tex.get_animation(is_diff and 14 or 10).attribute
local left_x = tex.get_animation(is_diff and 13 or 9).attribute
local center_width = tex.get_animation(is_diff and 15 or 11).attribute
local top_y = tex.get_animation(16).attribute
local center_height = tex.get_animation(17).attribute
local bottom_y = yellow_box_bottom_y()
local eh = edge_height()
tex.draw_texture(T("yellow_box", "yellow_box_bottom_right"), { x = right_x, y = bottom_y + y_offset, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_bottom_left"), { x = left_x, y = bottom_y + y_offset, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_top_right"), { x = right_x, y = top_y + y_offset, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_top_left"), { x = left_x, y = top_y + y_offset, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_bottom"), { x = left_x + eh, y = bottom_y + y_offset, x2 = center_width, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_right"), { x = right_x, y = top_y + eh + y_offset, y2 = center_height, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_left"), { x = left_x, y = top_y + eh + y_offset, y2 = center_height, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_top"), { x = left_x + eh, y = top_y + y_offset, x2 = center_width, fade = fade })
tex.draw_texture(T("yellow_box", "yellow_box_center"), { x = left_x + eh, y = top_y + eh + y_offset, x2 = center_width, y2 = center_height, fade = fade })
end
-- BaseBox::draw_closed — the generic folder_texture board (colour frame = texture_index).
-- BackBox and DanBox reuse this; SongBox/FolderBox call it before their own closed content.
function M.draw_base_closed(box)
local bx = box:box_x()
local by = box:box_y()
local fade = box.fade.attribute
local tex_index = box:texture_index()
local use_shader = box:has_recolor()
M.draw_shadows(bx, by, fade, 0)
if use_shader then box:begin_recolor() end
tex.draw_texture(T("box", "folder_texture_left"), { frame = tex_index, x = bx, y = by, fade = fade })
tex.draw_texture(T("box", "folder_texture"), { frame = tex_index, x = bx, y = by, x2 = song_box_bg_width(), fade = fade })
tex.draw_texture(T("box", "folder_texture_right"), { frame = tex_index, x = bx, y = by, fade = fade })
if use_shader then box:end_recolor() end
if tex_index == TI_DEFAULT then
tex.draw_texture(T("box", "genre_overlay"), { x = bx, y = by, fade = fade })
end
if box:genre_index() == GI_DIFFICULTY then
tex.draw_texture(T("box", "diff_overlay"), { x = bx, y = by, fade = fade })
end
end
-- The vertical song/folder title (BaseBox::load_text name). text_color is always white for
-- song/folder/back; the outline is the genre's fore_color. Cached per box on `self`.
function M.vertical_name(self, box)
self._name_cache = self._name_cache or {}
local name = box.text_name or ""
local fc = box:fore_color() or { r = 0, g = 0, b = 0, a = 255 }
local key = box:path() .. "\0" .. tostring(fc.r or 0) .. "." .. tostring(fc.g or 0) .. "." .. tostring(fc.b or 0)
local t = self._name_cache[key]
if t == nil then
local cfg = tex.get_skin_config("song_select_box_name")
local font_size = (cfg and cfg.font_size) or 40
if M.utf8_char_count(name) >= 30 then
font_size = font_size - 10 * tex.get_screen_scale()
end
local outline = { fc.r or 0, fc.g or 0, fc.b or 0, fc.a or 255 }
t = text.create_raw_text(name, font_size, { 255, 255, 255, 255 }, outline, true, 5.0, 2.0, 1.0)
self._name_cache[key] = t
end
return t
end
-- Highest-crown helper shared by SongBox/FolderBox closed states.
-- Returns frame (min(diff, URA)) and the crown value, or nil.
function M.highest_crown(map)
local highest_key = -1
for k in pairs(map) do
if type(k) == "number" and k > highest_key then highest_key = k end
end
if highest_key < 0 then return nil end
local frame = math.min(4, highest_key) -- Difficulty::URA == 4
return map[highest_key], frame
end
-- Draw a single crown icon (crown_dfc/crown_fc/crown_clear) at frame, position px, y.
-- crowns: {0=none,1=clear,2=fc,3=dfc}
function M.draw_crown(crown, frame, px, y, fade)
if crown == 3 then
tex.draw_texture(T("yellow_box", "crown_dfc"), { frame = frame, x = px, y = y, fade = fade })
elseif crown == 2 then
tex.draw_texture(T("yellow_box", "crown_fc"), { frame = frame, x = px, y = y, fade = fade })
elseif crown and crown >= 1 then
tex.draw_texture(T("yellow_box", "crown_clear"), { frame = frame, x = px, y = y, fade = fade })
end
end
-- Localized song count ("全{0}曲" / "{0} Songs").
function M.format_song_count(count)
local template = tex.skin_text("song_count")
if template == "" then template = "全{0}曲" end
local out, n = template:gsub("{0}", tostring(count))
if n == 0 then out = template .. tostring(count) end
return out
end
-- ---------------------------------------------------------------------------
-- Yellow-box animation driving. The engine no longer starts/advances the shared
-- yellow-box animations (9..18); the skin does it, keyed off the box draw_state.
-- ---------------------------------------------------------------------------
local YB_OPEN_ANIMS = { 9, 10, 11 }
local YB_DIFF_ANIMS = { 13, 14, 15, 16, 17, 18 }
local YB_RESET_ANIMS = { 9, 10, 11, 14, 16, 17 }
local function yb_anim(id) return tex.get_animation(id) end
local function yb_open() -- expand_box reset + create_anim (start 9/10/11)
for _, id in ipairs(YB_RESET_ANIMS) do yb_anim(id):reset() end
for _, id in ipairs(YB_OPEN_ANIMS) do yb_anim(id):start() end
end
local function yb_diff() -- create_anim_2 (start 13..18)
for _, id in ipairs(YB_DIFF_ANIMS) do yb_anim(id):start() end
end
local function yb_back() -- exit_box -> create_anim (reset 14/16/17 + start 9/10/11)
yb_anim(14):reset(); yb_anim(16):reset(); yb_anim(17):reset()
for _, id in ipairs(YB_OPEN_ANIMS) do yb_anim(id):start() end
end
-- Per-open-box state (only one box is open at a time).
local yb_box = { path = nil, state = "closed" }
function M.reset_yellow_box()
yb_box.path, yb_box.state = nil, "closed"
end
-- Call once per box from draw_box; detects open/enter/exit transitions.
function M.track_yellow_box(box)
local state = box:draw_state()
local path = box:path()
if state == "closed" then
if yb_box.path == path then
yb_box.path, yb_box.state = nil, "closed"
end
return
end
if yb_box.path ~= path then
yb_box.path, yb_box.state = path, state
yb_open()
if state == "diff_select" then yb_diff() end
elseif yb_box.state ~= state then
yb_box.state = state
if state == "diff_select" then yb_diff() else yb_back() end
end
end
-- Call once per frame from SongSelect:update; advances the shared yellow-box animations.
function M.update_yellow_box(current_ms)
for _, id in ipairs(YB_OPEN_ANIMS) do yb_anim(id):update(current_ms) end
for _, id in ipairs(YB_DIFF_ANIMS) do yb_anim(id):update(current_ms) end
end
return M
@@ -0,0 +1,379 @@
-- SongBox rendering (song board closed/open/diff-select), split out of song_select.lua.
-- Mirrors src/objects/song_select/file_navigator/box_song.cpp.
local base = require("song_select/file_navigator/box_base")
local SKIN = {
yb_diff_offset = { x = 60, y = -20 },
}
local T = base.T
local utf8_char_count = base.utf8_char_count
local draw_shadows = base.draw_shadows
local draw_yellow_box = base.draw_yellow_box
local draw_base_closed = base.draw_base_closed
local vertical_name = base.vertical_name
-- Difficulty enum (must match enums.h).
local DIFF_EASY, DIFF_NORMAL, DIFF_HARD, DIFF_ONI, DIFF_URA = 0, 1, 2, 3, 4
local BLACK = { 0, 0, 0, 255 }
local WHITE = { 255, 255, 255, 255 }
local BLANK = { 0, 0, 0, 0 }
local function is_2p_screen()
return (tex.current_screen and tex.current_screen() or "") == "SONG_SELECT_2P"
end
-- Build a set of vertical OutlinedText labels sized to fit one shared box (mirrors
-- build_vertical_label_set in box_song.cpp).
local function build_vertical_labels(keys, box_cfg, v_advance, font_bonus, fills, outlines, thickness)
local labels = {}
local max_chars = 1
for i, key in ipairs(keys) do
labels[i] = tex.skin_text(key)
if labels[i] == "" then labels[i] = "?" end
max_chars = math.max(max_chars, utf8_char_count(labels[i]))
end
local width_cap = box_cfg.width - 4.0
local height_cap = (box_cfg.height - 4.0) / (1.0 + (max_chars - 1) * v_advance)
local font_size = math.floor(math.min(width_cap, height_cap) + font_bonus)
local out = {}
for i = 1, #keys do
out[i] = text.create_raw_text(labels[i], font_size, fills[i], outlines[i], true, thickness, 2.0, v_advance)
end
return out
end
local function scaled_box(x, y, w, h)
local s = tex.get_screen_scale()
return { x = x * s, y = y * s, width = w * s, height = h * s }
end
local function diff_tower_label_box() return scaled_box(475, 158, 53, 152) end
local function difficulty_bar_label_box() return scaled_box(481, 256, 26, 96) end
-- diff_tower_label (5 diffs) — static cache in C++, cached on self here.
local function diff_tower_labels(self)
if self._diff_tower_labels then return self._diff_tower_labels end
local cfg = diff_tower_label_box()
local keys = { "diff_tower_easy", "diff_tower_normal", "diff_tower_hard", "diff_tower_oni", "diff_tower_ura" }
local fills = { BLACK, BLACK, BLACK, BLACK, WHITE }
local outlines = { BLANK, BLANK, BLANK, BLANK, { 0, 73, 90, 255 } }
self._diff_tower_labels = build_vertical_labels(keys, cfg, 0.9, 5.0, fills, outlines, 3.0)
return self._diff_tower_labels
end
-- difficulty_bar_label (4 diffs).
local function difficulty_bar_labels(self)
if self._difficulty_bar_labels then return self._difficulty_bar_labels end
local cfg = difficulty_bar_label_box()
local keys = { "diff_tower_easy", "diff_tower_normal", "diff_tower_hard", "diff_tower_oni" }
local colors = { BLACK, BLACK, BLACK, BLACK }
self._difficulty_bar_labels = build_vertical_labels(keys, cfg, 0.9, 7.0, colors, colors, 0.0)
return self._difficulty_bar_labels
end
-- The three song text objects (name, name_black, subtitle) from BaseBox::load_text +
-- SongBox::load_text, cached per box on self.
local function song_texts(self, box)
self._song_texts = self._song_texts or {}
local name = box.text_name or ""
local fc = box:fore_color() or { r = 0, g = 0, b = 0, a = 255 }
local key = box:path() .. "\0" .. tostring(fc.r or 0) .. "." .. tostring(fc.g or 0) .. "." .. tostring(fc.b or 0)
local t = self._song_texts[key]
if t == nil then
local sub = box.text_subtitle or ""
local fore = { fc.r or 0, fc.g or 0, fc.b or 0, fc.a or 255 }
local name_cfg = tex.get_skin_config("song_select_box_name") or { font_size = 40 }
local base_name_font = name_cfg.font_size or 40
local name_font = base_name_font
local name_outline = 5.0
if utf8_char_count(name) >= 30 then
name_font = base_name_font - 10 * tex.get_screen_scale()
name_outline = 5.0 * (name_font / base_name_font)
end
local name_text = text.create_raw_text(name, name_font, WHITE, fore, true, 5.0, 2.0, 1.0)
local name_black = text.create_raw_text(name, name_font, WHITE, BLACK, true, name_outline, 2.0, 1.0)
local sub_cfg = tex.get_skin_config("song_select_box_subtitle") or { font_size = 30 }
local base_sub_font = sub_cfg.font_size or 30
local sub_font = base_sub_font
local sub_outline = 5.0
if utf8_char_count(sub) >= 30 then
sub_font = base_sub_font - 10 * tex.get_screen_scale()
sub_outline = 5.0 * (sub_font / base_sub_font)
end
local subtitle = text.create_raw_text(sub, sub_font, WHITE, BLACK, true, sub_outline, 2.0, 1.0)
t = { name = name_text, name_black = name_black, subtitle = subtitle }
self._song_texts[key] = t
end
return t
end
-- draw_diff_outline (small crown outline; doubled in 2P).
local function draw_diff_outline(x, y, fade)
if is_2p_screen() then
local half = tex.get_texture_info("yellow_box", "s_crown_dfc")
half = (half and half.width or 0) * 0.35
tex.draw_texture(T("yellow_box", "s_crown_outline"), { x = x - half, y = y, fade = fade })
tex.draw_texture(T("yellow_box", "s_crown_outline"), { x = x + half, y = y, fade = fade })
else
tex.draw_texture(T("yellow_box", "s_crown_outline"), { x = x, y = y, fade = fade })
end
end
-- draw_diff_crown (small crown per diff; uses p1/p2 scores in 2P).
local function draw_diff_crown(box, diff, x, y, fade)
local function draw_one(info, px)
if not info.has_score then return end
if info.crown == 3 then
tex.draw_texture(T("yellow_box", "s_crown_dfc"), { x = px, y = y, fade = fade })
elseif info.crown == 2 then
tex.draw_texture(T("yellow_box", "s_crown_fc"), { x = px, y = y, fade = fade })
elseif info.crown >= 1 then
tex.draw_texture(T("yellow_box", "s_crown_clear"), { x = px, y = y, fade = fade })
end
end
if is_2p_screen() then
local half = tex.get_texture_info("yellow_box", "s_crown_dfc")
half = (half and half.width or 0) * 0.35
draw_one(box:course_info(diff), x - half)
draw_one(box:course_info_p2(diff), x + half)
else
draw_one(box:course_info(diff), x)
end
end
-- draw_box_crown (the closed-state best crown; highest-difficulty score wins).
local function draw_box_crown(box, x, y, fade)
local function highest(info_fn)
local best_key = -1
local best_crown = 0
for d = 0, 4 do
local info = info_fn(d)
if info.has_course and info.has_score then
if d > best_key then best_key = d; best_crown = info.crown end
end
end
if best_key < 0 then return nil, nil end
return best_crown, math.min(DIFF_URA, best_key)
end
local crown_1p, frame_1p = highest(function(d) return box:course_info(d) end)
local crown_2p, frame_2p
if is_2p_screen() then
crown_2p, frame_2p = highest(function(d) return box:course_info_p2(d) end)
end
if crown_2p then
local info = tex.get_texture_info("yellow_box", "crown_dfc")
local half = (info and info.width or 0) * 0.35
if crown_1p then base.draw_crown(crown_1p, frame_1p, x - half, y, fade) end
base.draw_crown(crown_2p, frame_2p, x + half, y, fade)
elseif crown_1p then
base.draw_crown(crown_1p, frame_1p, x, y, fade)
end
end
-- draw_text (the open-state name/subtitle, tracking the yellow box's expansion).
local function draw_text(box, texts)
local right_out = tex.get_animation(10)
local right_out_2 = tex.get_animation(14)
local top_y_out = tex.get_animation(16)
local x = box:box_x()
+ (right_out.attribute * 0.90 - right_out.start_position * 0.90)
+ right_out_2.attribute - right_out_2.start_position
local sub_cfg = tex.get_skin_config("song_select_box_subtitle") or { x = -15, y = 545, height = 410 }
local h = math.min(texts.subtitle.height, sub_cfg.height)
texts.subtitle:draw({
x = x + sub_cfg.x,
y = sub_cfg.y - h + top_y_out.attribute - top_y_out.start_position,
y2 = h - texts.subtitle.height,
fade = box.open_fade.attribute
})
local name_cfg = tex.get_skin_config("song_select_box_name") or { x = 47, y = 129, height = 417 }
local name_h = math.min(texts.name.height, name_cfg.height) - texts.name.height
local name_x = x + name_cfg.x - math.floor(texts.name.width / 2)
local name_y = name_cfg.y + top_y_out.attribute - top_y_out.start_position
texts.name_black:draw({ x = name_x, y = name_y, y2 = name_h, fade = box.open_fade.attribute })
texts.name:draw({ x = name_x, y = name_y, y2 = name_h, fade = 1 - box.open_fade.attribute })
end
function SongSelect:draw_song_closed(box)
draw_base_closed(box)
local bx = box:box_x()
local by = box:box_y()
local fade = box.fade.attribute
local name = vertical_name(self, box)
local name_cfg = tex.get_skin_config("song_select_box_name") or { x = 47, y = 129, height = 417 }
local name_x = bx + name_cfg.x - math.floor(name.width / 2)
local name_y = name_cfg.y + by
local name_h = math.min(name.height, name_cfg.height)
name:draw({ x = name_x, y = name_y, y2 = name_h - name.height, fade = fade })
if box:has_preimage() then
tex.draw_texture(T("yellow_box", "preimage_bg"), { x = bx, y = by, fade = fade })
local ps = tex.get_screen_scale()
box:draw_preimage(bx + 20 * ps, by + 30 * ps, 53 * ps, 53 * ps, fade)
elseif box:ex_data_flag() == 3 then -- limited_time
tex.draw_texture(T("yellow_box", "ex_data_limited_time_balloon"), { x = bx, y = by, fade = fade })
elseif box.is_new then
tex.draw_texture(T("yellow_box", "ex_data_new_song_balloon"), { x = bx, y = by, fade = fade })
end
draw_box_crown(box, bx, by, fade)
end
function SongSelect:draw_song_open(box)
local bx = box:box_x()
local by = box:box_y()
local open_fade = box.open_fade.attribute
draw_shadows(bx, by, open_fade, 1)
draw_yellow_box(false, 1.0, 0.0)
local offset = (SKIN.yb_diff_offset or { x = 60 }).x
local texts = song_texts(self, box)
-- diff outlines + crowns (diff < URA)
for d = 0, 3 do
local info = box:course_info(d)
if info.has_course then
draw_diff_outline(d * offset, 0.0, math.min(open_fade, 0.25))
draw_diff_crown(box, d, d * offset, 0.0, open_fade)
end
end
-- ex_data / new icon
local exd = box:ex_data_flag()
if exd == 1 then
tex.draw_texture(T("yellow_box", "ex_data_new_audio"), { fade = open_fade })
elseif exd == 2 then
tex.draw_texture(T("yellow_box", "ex_data_old_audio"), { fade = open_fade })
elseif exd == 3 then
tex.draw_texture(T("yellow_box", "ex_data_limited_time"), { fade = open_fade })
elseif box.is_new then
tex.draw_texture(T("yellow_box", "ex_data_new_song"), { fade = open_fade })
end
if tex.display_bpm() then
local bpm = box:bpm_text()
local cfg = tex.get_skin_config("song_select_box_bpm") or { x = 628, y = 120 }
if bpm then bpm:draw({ x = cfg.x, y = cfg.y, fade = open_fade }) end
end
if box.is_favorite then
tex.draw_texture(T("yellow_box", "favorite_" .. tex.player_num() .. "p"), { fade = open_fade })
end
-- difficulty bars (4)
for i = 0, 3 do
tex.draw_texture(T("yellow_box", "difficulty_bar"), { frame = i, x = i * offset, fade = open_fade })
if not box:course_info(i).has_course then
tex.draw_texture(T("yellow_box", "difficulty_bar_shadow"), { frame = i, x = i * offset, fade = math.min(open_fade, 0.25) })
end
end
-- difficulty bar labels
local labels = difficulty_bar_labels(self)
local box_cfg = difficulty_bar_label_box()
for i = 0, 3 do
local label = labels[i + 1]
label:draw({
x = box_cfg.x + i * offset + box_cfg.width / 2 - label.width / 2,
y = box_cfg.y,
y2 = math.min(label.height, box_cfg.height) - label.height,
fade = open_fade
})
end
-- stars + branch indicator (diff < URA)
local offset_y = (SKIN.yb_diff_offset or { y = -17 }).y
for d = 0, 3 do
local info = box:course_info(d)
if info.has_course then
for j = 0, info.level - 1 do
tex.draw_texture(T("yellow_box", "star"), { x = d * offset, y = j * offset_y, fade = open_fade })
end
if info.is_branching and (math.floor(tex.get_current_ms() / 1000) % 2 == 0) then
tex.draw_texture(T("yellow_box", "branch_indicator"), { x = d * offset, fade = open_fade })
end
end
end
draw_text(box, texts)
end
function SongSelect:draw_song_diff_select(box)
draw_yellow_box(true, 1.0, 0.0)
local diff_fade = box.diff_fade_in.attribute
tex.draw_texture(T("diff_select", "back"), { fade = diff_fade })
tex.draw_texture(T("diff_select", "option"), { fade = diff_fade })
tex.draw_texture(T("diff_select", "neiro"), { fade = diff_fade })
local s = tex.get_screen_scale()
local offset_x = 115 * s
local offset_y = -120 * s
local crown_offset = 8 * s
-- diff crowns (diff < URA)
for d = 0, 3 do
local info = box:course_info(d)
if info.has_course then
local cx = d * offset_x + crown_offset
draw_diff_outline(cx, offset_y, math.min(diff_fade, 0.25))
draw_diff_crown(box, d, cx, offset_y, diff_fade)
end
end
-- diff towers + labels
local tower_labels = diff_tower_labels(self)
local box_cfg = diff_tower_label_box()
for i = 0, 3 do
local label_index = i
if i == DIFF_ONI and box.is_ura then
tex.draw_texture(T("diff_select", "diff_tower"), { frame = 4, x = i * offset_x, fade = diff_fade })
tex.draw_texture(T("diff_select", "ura_oni_plate"), { fade = diff_fade })
label_index = 4
else
tex.draw_texture(T("diff_select", "diff_tower"), { frame = i, x = i * offset_x, fade = diff_fade })
end
if not box:course_info(i).has_course then
tex.draw_texture(T("diff_select", "diff_tower_shadow"), { frame = i, x = i * offset_x, fade = math.min(diff_fade, 0.25) })
end
local label = tower_labels[label_index + 1]
label:draw({
x = box_cfg.x + i * offset_x + (box_cfg.width - label.width) / 2,
y = box_cfg.y,
fade = diff_fade
})
end
-- ura stars + branch indicator
local star_offset_y = -20 * tex.get_screen_scale()
for d = 0, 4 do
local info = box:course_info(d)
if info.has_course then
if (d == DIFF_URA and not box.is_ura) or (d == DIFF_ONI and box.is_ura) then
-- skip
else
for j = 0, info.level - 1 do
tex.draw_texture(T("yellow_box", "star_ura"), { x = math.min(d, DIFF_ONI) * offset_x, y = j * star_offset_y, fade = diff_fade })
end
if info.is_branching and (math.floor(tex.get_current_ms() / 1000) % 2 == 0) then
local bt = (d == DIFF_URA) and T("yellow_box", "branch_indicator_ura") or T("yellow_box", "branch_indicator_diff")
tex.draw_texture(bt, { x = math.min(d, DIFF_ONI) * offset_x, fade = diff_fade })
end
end
end
end
draw_text(box, song_texts(self, box))
end
@@ -0,0 +1,186 @@
local SKIN = {
genre_bg_title = { y = -2000, width = 225 },
}
-- Inline-list background (GenreBG) drawing, split out of song_select.lua.
-- Mirrors src/objects/song_select/file_navigator/genre_bg.cpp.
local function T(name)
return tex.get_texture("song_select/box/" .. name)
end
local _bg_edge_width, _bg_folder_edge_width
local function bg_edge_width()
if _bg_edge_width == nil then
local info = tex.get_texture_info("box", "folder_background_edge")
_bg_edge_width = (info and info.width) or 0
end
return _bg_edge_width
end
local function bg_folder_edge_width()
if _bg_folder_edge_width == nil then
local info = tex.get_texture_info("box", "folder_background_folder_edge")
_bg_folder_edge_width = (info and info.width) or 0
end
return _bg_folder_edge_width
end
-- draw_anim (folder still entering; box's bounds drive the growing span).
local function draw_anim(gb, box)
if not box then return end
local s = gb.scale.attribute
local offset = 5 * s
local edge_width = bg_edge_width() * s
local center = (box.left_bound + box.right_bound) / 2.0
local half_width = (box.right_bound - box.left_bound) / 2.0
local base_start = center - half_width * s
local base_end = center + half_width * s
local exiting = (gb.move_left ~= nil and gb.move_right ~= nil)
local start_position = exiting and (base_start + gb.move_left.attribute) or base_start
local end_position = exiting and (base_end + gb.move_right.attribute) or (base_end + gb.move.attribute)
local frame = gb:texture_frame()
local fade = gb.fade.attribute
local sy = -(gb.stretch.attribute * s)
local recolor = gb:has_recolor()
if recolor then gb:begin_recolor() end
tex.draw_texture(T("folder_background_edge"), { frame = frame, scale = s, center = true, mirror = "horizontal", x = start_position - offset, y = sy, y2 = gb.stretch.attribute * s, fade = fade })
tex.draw_texture(T("folder_background"), { frame = frame, scale = s, center = true, x = start_position + edge_width - offset, y = sy, x2 = end_position - start_position - edge_width - offset, y2 = gb.stretch.attribute * s, fade = fade })
tex.draw_texture(T("folder_background_edge"), { frame = frame, scale = s, center = true, x = end_position - edge_width + offset, y = sy, y2 = gb.stretch.attribute * s, fade = fade })
local edge_width_top = bg_folder_edge_width() * s
local title_cfg = SKIN.genre_bg_title or { y = 0, width = 300 }
local name = gb:name()
local dest_width = math.min(title_cfg.width, name.width) * s
tex.draw_texture(T("folder_background_folder_edge"), { frame = frame, scale = s, center = true, mirror = "horizontal", x = center - dest_width / 2 - edge_width_top, y = sy, y2 = gb.stretch.attribute * s, fade = fade })
tex.draw_texture(T("folder_background_folder"), { frame = frame, scale = s, center = true, x = center - dest_width / 2, y = sy, x2 = dest_width, y2 = gb.stretch.attribute * s, fade = fade })
tex.draw_texture(T("folder_background_folder_edge"), { frame = frame, scale = s, center = true, x = center + dest_width / 2, y = sy, y2 = gb.stretch.attribute * s, fade = fade })
if recolor then gb:end_recolor() end
name:draw({ center = true, x = center - dest_width / 2, y = title_cfg.y - gb.stretch.attribute * s, x2 = dest_width - name.width, y2 = gb.stretch.attribute * s, fade = fade })
end
-- draw_exit_anim (folder closing; move_left/move_right drive the shrink).
local function draw_exit_anim(gb)
local s = gb.scale.attribute
local offset = 5 * s
local edge_width = bg_edge_width() * s
local anim_start = gb.move_left.attribute
local anim_end = gb.move_right.attribute
local frame = gb:texture_frame()
local fade = gb.fade.attribute
local recolor = gb:has_recolor()
if recolor then gb:begin_recolor() end
tex.draw_texture(T("folder_background_edge"), { frame = frame, scale = s, mirror = "horizontal", x = anim_start - offset, fade = fade })
tex.draw_texture(T("folder_background"), { frame = frame, scale = s, x = anim_start + edge_width - offset, x2 = anim_end - anim_start - edge_width - offset, fade = fade })
tex.draw_texture(T("folder_background_edge"), { frame = frame, scale = s, x = anim_end - edge_width + offset, fade = fade })
local edge_width_top = bg_folder_edge_width()
local title_cfg = SKIN.genre_bg_title or { width = 300 }
local name = gb:name()
local dest_width = math.min(title_cfg.width, name.width)
local center = 638.5 * tex.get_screen_scale()
tex.draw_texture(T("folder_background_folder_edge"), { frame = frame, mirror = "horizontal", x = center - dest_width / 2 - edge_width_top, fade = fade })
tex.draw_texture(T("folder_background_folder"), { frame = frame, x = center - dest_width / 2, x2 = dest_width, fade = fade })
tex.draw_texture(T("folder_background_folder_edge"), { frame = frame, x = center + dest_width / 2, fade = fade })
if recolor then gb:end_recolor() end
name:draw({ x = center - dest_width / 2, y = title_cfg.y, x2 = dest_width - name.width, fade = fade })
end
function SongSelect:draw_genre_bg(gb, start_position, end_position, folder)
if not gb:is_finished() then
draw_anim(gb, folder)
return
elseif not gb:is_complete() then
draw_exit_anim(gb)
return
end
-- Steady state: fill the span between the first and last song's bounds.
local screen_width = tex.get_screen_width()
local frame = gb:texture_frame()
local fade = gb.fade.attribute
local name = gb:name()
local title_cfg = SKIN.genre_bg_title or { y = 0, width = 300 }
local boundary = tex.get_skin_config("song_select_genre_bg_boundary") or { x = 442, width = 835 }
local edge_width_top = bg_folder_edge_width()
local dest_width = math.min(title_cfg.width, name.width)
local center = 638.5 * tex.get_screen_scale()
local bg_start_pos = start_position
local bg_end_pos = end_position
if boundary.x < start_position and start_position < center then
bg_start_pos = boundary.x
end
if center < end_position and end_position < boundary.width then
bg_end_pos = boundary.width
end
local recolor = gb:has_recolor()
if recolor then gb:begin_recolor() end
if bg_start_pos > bg_end_pos then
-- wraps around: two segments
local offset = 5
local edge_width = bg_edge_width()
local draw_end_l = math.min(bg_end_pos, screen_width)
if 0 < draw_end_l then
if bg_end_pos < screen_width then
tex.draw_texture(T("folder_background"), { frame = frame, x = 0, x2 = draw_end_l - edge_width + offset, fade = fade })
tex.draw_texture(T("folder_background_edge"), { frame = frame, x = draw_end_l - edge_width + offset, fade = fade })
else
tex.draw_texture(T("folder_background"), { frame = frame, x = 0, x2 = draw_end_l, fade = fade })
end
end
local draw_start_r = math.max(bg_start_pos, 0)
if draw_start_r < screen_width then
if bg_start_pos > 0 then
tex.draw_texture(T("folder_background"), { frame = frame, x = draw_start_r + edge_width - offset, x2 = screen_width, fade = fade })
tex.draw_texture(T("folder_background_edge"), { frame = frame, mirror = "horizontal", x = draw_start_r - offset, fade = fade })
else
tex.draw_texture(T("folder_background"), { frame = frame, x = draw_start_r, x2 = screen_width, fade = fade })
end
end
else
local offset = 5
local edge_width = bg_edge_width()
local draw_start = math.max(bg_start_pos, 0)
local draw_end = math.min(bg_end_pos, screen_width)
if draw_start < draw_end then
local fill_start = draw_start
local fill_end = draw_end
if bg_start_pos > 0 then
tex.draw_texture(T("folder_background_edge"), { frame = frame, mirror = "horizontal", x = draw_start - offset, fade = fade })
fill_start = draw_start + edge_width - offset
end
if bg_end_pos < screen_width then
tex.draw_texture(T("folder_background_edge"), { frame = frame, x = draw_end - edge_width + offset, fade = fade })
fill_end = draw_end - edge_width + offset
end
if fill_start < fill_end then
tex.draw_texture(T("folder_background"), { frame = frame, x = fill_start, x2 = fill_end - fill_start, fade = fade })
end
end
end
local center_not_covered = (start_position <= end_position)
and (center < start_position or end_position < center)
or (end_position < center and center < start_position)
if center_not_covered then
if recolor then gb:end_recolor() end
return
end
tex.draw_texture(T("folder_background_folder_edge"), { frame = frame, mirror = "horizontal", x = center - dest_width / 2 - edge_width_top, fade = fade })
tex.draw_texture(T("folder_background_folder"), { frame = frame, x = center - dest_width / 2, x2 = dest_width, fade = fade })
tex.draw_texture(T("folder_background_folder_edge"), { frame = frame, x = center + dest_width / 2, fade = fade })
if recolor then gb:end_recolor() end
name:draw({ x = center - dest_width / 2, y = title_cfg.y, x2 = dest_width - name.width, fade = fade })
end
+249
View File
@@ -0,0 +1,249 @@
local SKIN_S = tex.get_screen_scale()
local SKIN = {
modifier_offset = { y = 50 * SKIN_S },
modifier_offset_2 = { x = 92 * SKIN_S, y = 110 * SKIN_S },
modifier_text = { x = 330 * SKIN_S, y = 823 * SKIN_S, font_size = math.floor(30 * SKIN_S) },
neiro_counter_margin = { x = 20 * SKIN_S },
neiro_counter_x_offset = { x = 60 * SKIN_S },
neiro_text = { x = 176.25, y = 844.5, font_size = 37 },
option_p2 = { x = 800 * SKIN_S },
option_text_in = { x = 100 * SKIN_S },
}
-- Default 演奏オプション (modifier) and 音色 (neiro) panels. The engine's ModifierSelector /
-- NeiroSelector keep the state (input, audio, animations); this draws them.
--
-- Not a SongSelect class: SongSelectScript installs draw_option_panel from here when the skin's
-- own SongSelect does not define one, so every skin gets this look unless it draws the panel itself
-- (a skin's draw_option_panel can return false to fall back to this one). The dan select screen
-- also calls draw_modifier directly.
local M = {}
local WHITE, BLACK = { 255, 255, 255, 255 }, { 0, 0, 0, 255 }
local function sprite(subset, name)
return tex.get_texture("song_select/" .. subset .. "/" .. name)
end
local function make_text(str, font_size, thickness)
return text.create_raw_text(str, font_size, WHITE, BLACK, false, thickness, 2.0)
end
-- per player and panel: the texts that slide when a value changes (see the engine's
-- ModifierSelector::start_text_animation), rebuilt whenever a new selector opens
local states = {}
local function panel_state(kind, player_num, change, init)
local key = kind .. player_num
local st = states[key]
if not st or st.id ~= change.id then
st = { id = change.id, seq = change.seq }
init(st)
states[key] = st
end
return st
end
local ICON = {
auto = "mod_auto",
speed = "mod_baisaku",
display = "mod_doron",
inverse = "mod_abekobe",
random = "mod_kimagure",
}
-- primary fades with `fade` and slides by `move`; secondary comes in from the opposite side
local function draw_animated(primary, secondary, x, y, animate, change)
if animate and change.active then
primary:draw({ x = x + change.move * change.dir, y = y, fade = change.fade })
secondary:draw({ x = (change.dir * -SKIN.option_text_in.x) + x + change.move * change.dir,
y = y, fade = 1.0 - change.fade })
else
primary:draw({ x = x, y = y })
end
end
-- how far past the top the panel leaves (the character rides along with it)
function M.slide_out()
return -237
end
function M.draw_modifier(sel)
local rows = sel:rows()
local change = sel:change()
local p = sel:player_num()
local cur = sel:index()
local font = SKIN.modifier_text.font_size
local function mk(s) return make_text(s, font, 3.5) end
local st = panel_state("mod", p, change, function(s)
local function label(k) return tex.skin_text(k) end
local function row_value(name)
for _, r in ipairs(rows) do if r.name == name then return r.value end end
return ""
end
s.prim = {
["true"] = mk(label("modifier_text_true")), ["false"] = mk(label("modifier_text_false")),
speed = mk(row_value("speed")), kimagure = mk(label("modifier_text_kimagure")),
detarame = mk(label("modifier_text_detarame")), neiro = mk(row_value("neiro")),
}
s.sec = {
["true"] = mk(label("modifier_text_true")), ["false"] = mk(label("modifier_text_false")),
speed = mk(row_value("speed")), kimagure = mk(label("modifier_text_kimagure")),
detarame = mk(label("modifier_text_detarame")), neiro = mk(row_value("neiro")),
}
s.names = {}
end)
if st.seq ~= change.seq then
st.seq = change.seq
local row = rows[cur]
if row then
local k
if row.name == "speed" or row.name == "neiro" then
k = row.name
elseif row.name == "random" then
if row.state == 1 then k = "kimagure" elseif row.state == 2 then k = "detarame" end
else
k = (row.state == 1) and "true" or "false"
end
if k then
st.sec[k] = st.prim[k]
st.prim[k] = mk(row.value)
end
end
end
local x = (p - 1) * SKIN.option_p2.x
local move_y = sel:offset()
local pitch = SKIN.modifier_offset.y
local txt = SKIN.modifier_text
local o2 = SKIN.modifier_offset_2
local n = #rows
tex.draw_texture(sprite("modifier", "top"), { x = x, y = move_y })
tex.draw_texture(sprite("modifier", p .. "p"), { x = x, y = move_y })
tex.draw_texture(sprite("modifier", "bottom"), { x = x, y = move_y + n * pitch })
for i = 1, n do
local row = rows[i]
local row_y = move_y + (i - 1) * pitch
local current = (i == cur)
tex.draw_texture(sprite("modifier", "background"), { x = x, y = row_y })
tex.draw_texture(sprite("modifier", current and "mod_bg_highlight" or "mod_bg"), { x = x, y = row_y })
tex.draw_texture(sprite("modifier", "mod_box"), { x = x, y = row_y })
local name = st.names[row.label]
if not name then
name = mk(row.label)
st.names[row.label] = name
end
name:draw({ x = o2.x + x, y = txt.y + row_y })
local text_y = txt.y + row_y
local function value_text(prim, sec)
draw_animated(prim, sec, txt.x - prim.width / 2.0 + x, text_y, current, change)
end
if row.name == "speed" then
value_text(st.prim.speed, st.sec.speed)
local spd = row.state
if spd >= 40 then tex.draw_texture(sprite("modifier", "mod_yonbai"), { x = x, y = row_y })
elseif spd >= 30 then tex.draw_texture(sprite("modifier", "mod_sanbai"), { x = x, y = row_y })
elseif spd > 10 then tex.draw_texture(sprite("modifier", ICON.speed), { x = x, y = row_y }) end
elseif row.name == "neiro" then
value_text(st.prim.neiro, st.sec.neiro)
elseif row.name == "random" then
if row.state == 1 then
value_text(st.prim.kimagure, st.sec.kimagure)
tex.draw_texture(sprite("modifier", ICON.random), { x = x, y = row_y })
elseif row.state == 2 then
value_text(st.prim.detarame, st.sec.detarame)
tex.draw_texture(sprite("modifier", "mod_detarame"), { x = x, y = row_y })
else
value_text(st.prim["false"], st.sec["false"])
end
else
local on = (row.state == 1)
if on and ICON[row.name] then
tex.draw_texture(sprite("modifier", ICON[row.name]), { x = x, y = row_y })
end
value_text(on and st.prim["true"] or st.prim["false"], on and st.sec["true"] or st.sec["false"])
end
if current then
tex.draw_texture(sprite("modifier", "blue_arrow"),
{ x = x - change.arrow_move, y = row_y, fade = change.arrow_fade })
tex.draw_texture(sprite("modifier", "blue_arrow"),
{ mirror = "horizontal", x = x + o2.y + change.arrow_move, y = row_y, fade = change.arrow_fade })
end
end
end
function M.draw_neiro(sel)
local names = sel:names()
local idx = sel:index()
local change = sel:change()
local p = sel:player_num()
local txt = SKIN.neiro_text
local in_x = SKIN.option_text_in.x
local function mk(s) return make_text(s, txt.font_size, 5.0) end
local st = panel_state("neiro", p, change, function(s)
s.text, s.text_2 = mk(names[idx] or ""), mk(names[idx] or "")
end)
if st.seq ~= change.seq then
st.seq = change.seq
st.text = st.text_2
st.text_2 = mk(names[idx] or "")
end
local x = (p - 1) * SKIN.option_p2.x
local y = sel:offset()
local dir = change.dir
local mv = change.move
tex.draw_texture(sprite("neiro", "background"), { x = x, y = y })
tex.draw_texture(sprite("neiro", p .. "p"), { x = x, y = y })
tex.draw_texture(sprite("neiro", "divisor"), { x = x, y = y })
tex.draw_texture(sprite("neiro", "music_note"), { x = x + mv * dir, y = y, fade = change.fade })
tex.draw_texture(sprite("neiro", "music_note"),
{ x = x + (dir * -in_x) + mv * dir, y = y, fade = 1.0 - change.fade })
tex.draw_texture(sprite("neiro", "blue_arrow"), { x = x - change.arrow_move, y = y, fade = change.arrow_fade })
tex.draw_texture(sprite("neiro", "blue_arrow"),
{ mirror = "horizontal", x = x + in_x * 2 + change.arrow_move, y = y, fade = change.arrow_fade })
local margin = SKIN.neiro_counter_margin.x
local counter = tostring(idx)
local total_width = #counter * margin
for i = 1, #counter do
local digit = counter:byte(i) - 48
tex.draw_texture(sprite("neiro", "counter"),
{ frame = digit, x = x - total_width / 2 + (i - 1) * margin, y = y })
end
counter = tostring(#names)
total_width = #counter * margin
for i = 1, #counter do
local digit = counter:byte(i) - 48
tex.draw_texture(sprite("neiro", "counter"),
{ frame = digit, x = x - total_width / 2 + (i - 1) * margin + SKIN.neiro_counter_x_offset.x, y = y })
end
st.text:draw({ x = x + txt.x - st.text.width / 2.0 + mv * dir, y = y + txt.y, fade = change.fade })
st.text_2:draw({ x = x + (dir * -in_x) + txt.x - st.text_2.width / 2.0 + mv * dir,
y = y + txt.y, fade = 1.0 - change.fade })
end
-- Installed as SongSelect:draw_option_panel. kind: 1 = modifier, 2 = the standalone neiro panel.
function M.draw_option_panel(_self, player, kind)
if kind == 1 then
local sel = player:modifier_panel()
if sel then M.draw_modifier(sel) end
else
local sel = player:neiro_panel()
if sel then M.draw_neiro(sel) end
end
end
return M