diff --git a/Graphics/game/balloon/rainbow_mask.png b/Graphics/game/balloon/rainbow_mask.png deleted file mode 100644 index 2cf7356..0000000 --- a/Graphics/game/balloon/rainbow_mask.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7333dfe9abb5330a56b69861c3b77d1e8db4e2d4fd407fd7d5b7fbca30cf2538 -size 1494 diff --git a/Graphics/game/balloon/texture.json b/Graphics/game/balloon/texture.json index 035483b..be80b51 100644 --- a/Graphics/game/balloon/texture.json +++ b/Graphics/game/balloon/texture.json @@ -1,94 +1,90 @@ -{ - "bubble": { - "x": 0, - "y": 0 - }, - "counter": { - "x": 0, - "y": 0, - "crop": [ - [ - 0, - 0, - 1.5, - 1.5 - ], - [ - 1.5, - 0, - 1.5, - 1.5 - ], - [ - 3, - 0, - 1.5, - 1.5 - ], - [ - 4.5, - 0, - 1.5, - 1.5 - ], - [ - 6, - 0, - 1.5, - 1.5 - ], - [ - 7.5, - 0, - 1.5, - 1.5 - ], - [ - 9, - 0, - 1.5, - 1.5 - ], - [ - 10.5, - 0, - 1.5, - 1.5 - ], - [ - 12, - 0, - 1.5, - 1.5 - ], - [ - 13.5, - 0, - 1.5, - 1.5 - ] - ] - }, - "pop": [ - { - "x2": 0, - "y2": 0 - }, - { - "x2": 0, - "y2": 0 - } - ], - "rainbow": { - "x": 189, - "y": -174 - }, - "rainbow_mask": { - "x": 189, - "y": -174 - }, - "explosion": { - "x2": 0, - "y2": 0 - } +{ + "bubble": { + "x": 0, + "y": 0 + }, + "counter": { + "x": 0, + "y": 0, + "crop": [ + [ + 0, + 0, + 1.5, + 1.5 + ], + [ + 1.5, + 0, + 1.5, + 1.5 + ], + [ + 3, + 0, + 1.5, + 1.5 + ], + [ + 4.5, + 0, + 1.5, + 1.5 + ], + [ + 6, + 0, + 1.5, + 1.5 + ], + [ + 7.5, + 0, + 1.5, + 1.5 + ], + [ + 9, + 0, + 1.5, + 1.5 + ], + [ + 10.5, + 0, + 1.5, + 1.5 + ], + [ + 12, + 0, + 1.5, + 1.5 + ], + [ + 13.5, + 0, + 1.5, + 1.5 + ] + ] + }, + "pop": [ + { + "x2": 0, + "y2": 0 + }, + { + "x2": 0, + "y2": 0 + } + ], + "rainbow": { + "x": 189, + "y": -174 + }, + "explosion": { + "x2": 0, + "y2": 0 + } } \ No newline at end of file diff --git a/Graphics/skin_config.json b/Graphics/skin_config.json index d5c66a1..2b121a1 100644 --- a/Graphics/skin_config.json +++ b/Graphics/skin_config.json @@ -4,132 +4,58 @@ "width": 960, "height": 720, "options": { - "delay_score_addition": true, "horizontal_song_select": false, "one_menu_sort": false, "option_skip_row": true, "option_neiro_row": false, - "dan_result_rank_plate": false, - "dan_game_rank_plate": false, - "dan_select_rank_plate": false, "entry_credit_arcade": true, - "dan_confirm_yes_left": false, - "costume_preset_slots": false, "dan_title_horizontal": false, "dan_chip_name_black": false, - "dan_exam_grid": false, - "dan_select_total_notes": true, - "dan_exam_value_center": true, - "combo_color_tiers": false, - "gauge_cell_fade_in": false, - "disable_chara_3d": true + "disable_chara_3d": true, + "loading_countdown": true } }, - "balloon_counter_2p_offset": { + "game_balloon_counter_2p_offset": { "x": 0, "y": 99 }, - "balloon_counter_margin": { + "game_balloon_counter_margin": { "x": 36 }, - "balloon_offset": { + "game_balloon_offset": { "x": 0 }, - "branch_indicator_y_offset": { + "game_branch_indicator_y_offset": { "y": 0 }, - "chara_entry": { - "x": 0, - "y": 2000 - }, - "combo_announce_add_x": { - "x": 409.5 - }, - "combo_announce_hundreds_offset": { - "x": 0 - }, - "combo_announce_number_thousands_x": { - "x": -48 - }, - "combo_announce_thousands_add_x": { - "x": 265.5 - }, - "combo_announce_thousands_offset": { - "x": 0 - }, - "combo_margin": { - "x": 30, - "y": 42 - }, - "dan_chip_name": { + "dan_select_chip_name": { "x": 42.3, "y": 159.1, "height": 375.3, "font_size": 36 }, - "dan_exam_info": { + "dan_select_exam_info": { "width": 846.0, "x": 19.8, "y": 80 }, - "dan_exam_info_game": { + "game_dan_exam_info": { "x": 16.5, "y": 96, "width": 705 }, - "dan_exam_value_center": { - "x": 295, - "width": -5 - }, - "dan_game_hori_name": { - "x": 115.5, - "y": 433.5, - "width": 206.25 - }, - "dan_hori_name": { - "x": 296.6, - "y": 118.6, - "width": 247.5 - }, - "dan_level_counter_margin": { - "x": 9.0 - }, - "dan_result_chara": { - "x": 100, - "y": 322 - }, - "dan_result_nameplate": { - "x": 296, - "y": 3, - "height": 200.0 - }, - "dan_ribbon": { - "x": 440.6, - "width": 135.0 - }, - "dan_ribbon_side": { - "x": 296.6, - "y": 288.4 - }, - "dan_select_indicator": { - "x": 260, - "y": 560 - }, - "dan_subtitle": { + "dan_select_subtitle": { "x": 464.0, "y": 515.5, "height": 360.0, "font_size": 27 }, - "dan_title": { + "dan_select_title": { "x": 504.5, "y": 157.3, "height": 360.0, "font_size": 36 }, - "dan_yellow_box_offset": { - "x": 126.0 - }, "diff_tower_easy": { "text": { "ja": " ", @@ -175,32 +101,18 @@ "ko": " " } }, - "drumroll_counter_margin": { + "game_drumroll_counter_margin": { "x": 42 }, - "drumroll_width_offset": { + "game_drumroll_width_offset": { "width": -108 }, - "entry_box_highlight_offset": { - "x": -30 - }, "entry_box_spacing": { "x": 4 }, "entry_box_text": { "font_size": 32 }, - "entry_box_text_offset": { - "y": 29 - }, - "entry_chara_offset_l": { - "x": 250, - "y": 965 - }, - "entry_chara_offset_r": { - "x": 250, - "y": 965 - }, "entry_costume": { "text": { "en": "", @@ -208,25 +120,7 @@ "zh": "", "ko": "" } - }, - "entry_drum_offset": { - "x": 0, - "y": 480 - }, - "entry_overlay_entry": { - "x": 0, - "y": -2000 - }, - "entry_overlay_title": { - "x": 0, - "y": -2000 - }, - "exam_counter_margin": { - "x": 18.0 - }, - "exam_gauge_offset": { - "x": -7.2 - }, + }, "free_play": { "x": 0, "y": 682, @@ -238,42 +132,18 @@ "ko": "프리 플레이" } }, - "game_chara_balloon": { - "x": -3000, - "y": 0 - }, - "game_chara_p1": { - "x": -3000, - "y": 0 - }, - "game_chara_p2": { - "x": -3000, - "y": 0 - }, - "game_nameplate_1p": { - "x": -6000, - "y": 0 - }, - "game_nameplate_2p": { - "x": -6000, - "y": 0 - }, "game_p2_y": { "y": 346.5 }, - "gauge_cells": { + "game_gauge_cells": { "x": 50 }, - "gauge_hit_effect_note": { + "game_gauge_hit_effect_note": { "x": 814.5, "y": -138, "height": 384 }, - "genre_bg_title": { - "y": -2000, - "width": 225 - }, - "gogo_fire_fade": { + "game_gogo_fire_fade": { "x": 1.0 }, "hit_taiko_to_start": { @@ -288,23 +158,6 @@ "ko": " " } }, - "indicator_don_offset": { - "x": 192.6 - }, - "indicator_entry_left": { - "x": 0, - "y": 2000 - }, - "indicator_entry_right": { - "x": 0, - "y": 2000 - }, - "indicator_face_offset": { - "x": 157.5 - }, - "indicator_kat_offset": { - "x": 20.7 - }, "indicator_text": { "x": 101.7, "font_size": 23, @@ -315,27 +168,7 @@ "ko": "테두리로 선택 면으로 결정" } }, - "judge_counter_1": { - "x": 228.15, - "y": 453.375 - }, - "judge_counter_2": { - "x": 157.95, - "y": 490.29 - }, - "judge_counter_3": { - "x": 236.925, - "y": 528.18 - }, - "judge_counter_4": { - "x": 528.18, - "y": 411.48, - "width": 590.04 - }, - "judge_counter_margin": { - "x": 20.175 - }, - "judge_pos": { + "game_judge_pos": { "x": 210, "y": 96 }, @@ -555,62 +388,24 @@ "height": 33, "font_size": 4 }, - "moji": { + "game_moji": { "x": 0, "y": 148.5 }, - "moji_drumroll": { + "game_moji_drumroll": { "x": 71.25, "y": 36, "width": 0 }, - "nameplate_entry": { - "x": 0, - "y": 2000 - }, - "nameplate_entry_left": { - "x": 0, - "y": 2000 - }, - "nameplate_entry_right": { - "x": 0, - "y": 2000 - }, - "neiro_text": { - "x": 176.25, - "y": 844.5, - "font_size": 37.5 - }, - "note_arc_curve_height": { - "height": 244.5 - }, - "note_arc_start_x_offset": { - "x": 150 - }, - "notes": { + "game_notes": { "y": 36 }, - "practice_menu_panel": { - "y": 188.0001, - "width": 705, - "height": 345 - }, - "practice_progress_bar_width": { + "game_practice_progress_bar_width": { "width": 915 }, - "score_counter_2p_y_offset": { + "game_score_counter_2p_y_offset": { "y": 213 - }, - "score_counter_animation_start_y": { - "y": -91.5 - }, - "score_counter_margin": { - "x": 30 - }, - "score_counter_pos": { - "x": 913.5, - "y": -16.5 - }, + }, "skip_counter": { "x": 480, "y": 600, @@ -629,33 +424,24 @@ "y": 600, "font_size": 33 }, - "song_box_bg": { + "song_select_box_bg": { "width": 59 }, - "song_box_name": { + "song_select_box_name": { "x": 48, "y": 136, "height": 416, "font_size": 32 }, - "song_info": { + "game_song_info": { "x": 915, "y": 355.5, "font_size": 36, "outline": 0 }, - "song_info_max_width": { + "game_song_info_max_width": { "width": 555 }, - "song_info_result": { - "x": -3000, - "y": -3000, - "font_size": 24 - }, - "song_info_result_subtitle": { - "y": -3000, - "font_size": 20 - }, "song_num": { "x": -2000, "y": 28, @@ -667,47 +453,21 @@ "ko": "{0}곡째" } }, - "song_num_result": { - "x": -3000, - "y": -3000 + "chara_3d": { + "entry": [0, 2000], + "song_select_1p": [0, 2000], + "song_select_2p": [0, 2000], + "dan_result": [100, 322], + "game_p1": [-3000, 0], + "game_p2": [-3000, 0], + "game_balloon": [-3000, 0] }, - "song_select_chara_1p": { - "x": 0, - "y": 2000 - }, - "song_select_chara_2p": { - "x": 0, - "y": 2000 - }, - "song_select_indicator": { - "x": 0, - "y": 2000 - }, - "song_select_nameplate_1p": { - "x": 0, - "y": 2000 - }, - "song_select_nameplate_2p": { - "x": 0, - "y": 2000 - }, - "song_select_offset": { - "x": -237 - }, - "total_notes_counter_margin": { - "x": 22.5 - }, - "yb_diff_offset": { - "x": 60, - "y": -20 - }, - "yellow_box_bottom": { - "y": 528 - }, - "loading_countdown": { - "x": 480.0, - "y": 530.0, - "width": 24.75, - "height": 10 + "nameplate": { + "entry": [0, 2000], + "song_select_1p": [0, 2000], + "song_select_2p": [0, 2000], + "dan_result": [296, 3], + "game_p1": [-6000, 0], + "game_p2": [-6000, 0] } } \ No newline at end of file diff --git a/README.md b/README.md index af641c3..748b137 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Scripts/background/background.lua b/Scripts/background/background.lua index 008ab3c..78a6e99 100644 --- a/Scripts/background/background.lua +++ b/Scripts/background/background.lua @@ -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 } diff --git a/Scripts/dan_select/dan_select.lua b/Scripts/dan_select/dan_select.lua new file mode 100644 index 0000000..0b884c2 --- /dev/null +++ b/Scripts/dan_select/dan_select.lua @@ -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 diff --git a/Scripts/entry/box.lua b/Scripts/entry/box.lua index d847a03..f4e8aca 100644 --- a/Scripts/entry/box.lua +++ b/Scripts/entry/box.lua @@ -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 diff --git a/Scripts/entry/player.lua b/Scripts/entry/player.lua index 6ce3b6e..2ed2171 100644 --- a/Scripts/entry/player.lua +++ b/Scripts/entry/player.lua @@ -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 diff --git a/Scripts/game/combo.lua b/Scripts/game/combo.lua new file mode 100644 index 0000000..e8b6ff9 --- /dev/null +++ b/Scripts/game/combo.lua @@ -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 diff --git a/Scripts/game/combo_announce.lua b/Scripts/game/combo_announce.lua new file mode 100644 index 0000000..9622b19 --- /dev/null +++ b/Scripts/game/combo_announce.lua @@ -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 diff --git a/Scripts/game/dan_game_draw.lua b/Scripts/game/dan_game_draw.lua new file mode 100644 index 0000000..cc4e5d0 --- /dev/null +++ b/Scripts/game/dan_game_draw.lua @@ -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 diff --git a/Scripts/game/judge_counter.lua b/Scripts/game/judge_counter.lua new file mode 100644 index 0000000..9dd4541 --- /dev/null +++ b/Scripts/game/judge_counter.lua @@ -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 diff --git a/Scripts/game/note_arc.lua b/Scripts/game/note_arc.lua new file mode 100644 index 0000000..bb2abbe --- /dev/null +++ b/Scripts/game/note_arc.lua @@ -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 diff --git a/Scripts/game/practice_menu.lua b/Scripts/game/practice_menu.lua new file mode 100644 index 0000000..f8200ab --- /dev/null +++ b/Scripts/game/practice_menu.lua @@ -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 diff --git a/Scripts/game/score_counter.lua b/Scripts/game/score_counter.lua new file mode 100644 index 0000000..9a7cc68 --- /dev/null +++ b/Scripts/game/score_counter.lua @@ -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 diff --git a/Scripts/global/entry_overlay.lua b/Scripts/global/entry_overlay.lua new file mode 100644 index 0000000..d98c4ec --- /dev/null +++ b/Scripts/global/entry_overlay.lua @@ -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 diff --git a/Scripts/global/indicator.lua b/Scripts/global/indicator.lua new file mode 100644 index 0000000..55e8a6b --- /dev/null +++ b/Scripts/global/indicator.lua @@ -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 diff --git a/Scripts/global/timer.lua b/Scripts/global/timer.lua index 1de8980..20f7bc0 100644 --- a/Scripts/global/timer.lua +++ b/Scripts/global/timer.lua @@ -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 diff --git a/Scripts/loading/loading_countdown.lua b/Scripts/loading/loading_countdown.lua new file mode 100644 index 0000000..33e08e6 --- /dev/null +++ b/Scripts/loading/loading_countdown.lua @@ -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 diff --git a/Scripts/result/dan_result_draw.lua b/Scripts/result/dan_result_draw.lua index 26dea25..2242ca5 100644 --- a/Scripts/result/dan_result_draw.lua +++ b/Scripts/result/dan_result_draw.lua @@ -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 diff --git a/Scripts/result/result_player.lua b/Scripts/result/result_player.lua index a435d25..f2c5dc7 100644 --- a/Scripts/result/result_player.lua +++ b/Scripts/result/result_player.lua @@ -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 diff --git a/Scripts/song_select/file_navigator/box_base.lua b/Scripts/song_select/file_navigator/box_base.lua new file mode 100644 index 0000000..303b156 --- /dev/null +++ b/Scripts/song_select/file_navigator/box_base.lua @@ -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 diff --git a/Scripts/song_select/file_navigator/box_song.lua b/Scripts/song_select/file_navigator/box_song.lua new file mode 100644 index 0000000..9d70b9b --- /dev/null +++ b/Scripts/song_select/file_navigator/box_song.lua @@ -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 diff --git a/Scripts/song_select/file_navigator/genre_bg.lua b/Scripts/song_select/file_navigator/genre_bg.lua new file mode 100644 index 0000000..1c895c9 --- /dev/null +++ b/Scripts/song_select/file_navigator/genre_bg.lua @@ -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 diff --git a/Scripts/song_select/option_panel.lua b/Scripts/song_select/option_panel.lua new file mode 100644 index 0000000..4907959 --- /dev/null +++ b/Scripts/song_select/option_panel.lua @@ -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