Required changes #2
No files matched your search
Binary file not shown.
@@ -83,10 +83,6 @@
|
||||
"x": 189,
|
||||
"y": -174
|
||||
},
|
||||
"rainbow_mask": {
|
||||
"x": 189,
|
||||
"y": -174
|
||||
},
|
||||
"explosion": {
|
||||
"x2": 0,
|
||||
"y2": 0
|
||||
|
||||
+41
-274
@@ -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": "",
|
||||
@@ -209,20 +121,6 @@
|
||||
"ko": ""
|
||||
}
|
||||
},␍
|
||||
"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,
|
||||
@@ -234,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": {
|
||||
@@ -284,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,
|
||||
@@ -311,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
|
||||
},
|
||||
@@ -551,59 +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_margin": {
|
||||
"x": 30
|
||||
},
|
||||
"score_counter_pos": {
|
||||
"x": 913.5,
|
||||
"y": -16.5
|
||||
},
|
||||
"skip_counter": {
|
||||
"x": 480,
|
||||
"y": 600,
|
||||
@@ -622,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,
|
||||
@@ -660,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]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user