local SKIN_S = tex.get_screen_scale() local SKIN = { result_score_margin = { x = 23 * SKIN_S }, } ResultPlayer = {} ResultPlayer.__index = ResultPlayer function ResultPlayer.new(player_num, is_2p, has_2p, gauge_length, difficulty, selected_difficulty, crown_type, score_diff, score_method, mod_display, mod_inverse, mod_random, mod_speed) local self = setmetatable({}, ResultPlayer) self.player_num = player_num local timing = tex.timing_offset(player_num) if timing then self.timing_text = text.create_raw_text(timing, 20, {255, 255, 255, 255}, {0, 0, 0, 255}, false, 3.0) end self.is_2p = is_2p self.has_2p = has_2p self.gauge_length = gauge_length self.difficulty = difficulty self.selected_difficulty = selected_difficulty self.crown_type = crown_type self.score_diff = score_diff self.score_method = score_method self.mod_display = mod_display self.mod_inverse = mod_inverse self.mod_random = mod_random self.mod_speed = mod_speed local ART = { [0] = 0, [1] = 1, [2] = 1, [3] = 2 } local NORMA = { [0] = 6000, [1] = 7000, [2] = 7000, [3] = 8000 } local d = math.min(math.max(difficulty, 0), 3) self.art_index = ART[d] if gauge_length == 87 then self.gauge_state = 2 elseif gauge_length / 87.0 * 10000.0 >= NORMA[d] - 0.5 then self.gauge_state = 1 else self.gauge_state = 0 end if self.art_index >= 2 then self.diff_str = "_hard" elseif self.art_index >= 1 then self.diff_str = "_normal" else self.diff_str = "_easy" end local p = tostring(player_num) .. "p" self.player_str = p self.score = "" self.good = "" self.ok = "" self.bad = "" self.max_combo = "" self.total_drumroll = "" self.fade_in_finished_flag = false self.gauge_started = false self.score_delay = nil self.fade_in_started = false self.rainbow_start_ms = -1.0 self.rainbow_frac = 0.0 -- Sub-objects self.bottom_chars = BottomCharacters.new() self.crown_obj = nil self.crown_msg_obj = nil self.basin_obj = nil self.high_score_obj = nil -- Animations owned by ResultPlayer self.fade_in_bottom = tex.get_animation(1, "result") self.gauge_fade_in = tex.get_animation(17, "result") self.tamashii_fire = tex.get_animation(20, "result") self.t_grad_clear = tex.get_texture("result/background/gradient_clear") self.t_grad_fail = tex.get_texture("result/background/gradient_fail") self.t_overlay = tex.get_texture("result/score/overlay") self.t_difficulty = tex.get_texture("result/score/difficulty") self.t_judge_good = tex.get_texture("result/score/judge_good") self.t_judge_ok = tex.get_texture("result/score/judge_ok") self.t_judge_bad = tex.get_texture("result/score/judge_bad") self.t_judge_num = tex.get_texture("result/score/judge_num") self.t_max_combo = tex.get_texture("result/score/max_combo") self.t_drumroll = tex.get_texture("result/score/drumroll") self.t_score_lbl = tex.get_texture("result/score/score") self.t_score_shinuchi_lbl = tex.get_texture("result/score/score_shinuchi") self.t_score_num = tex.get_texture("result/score/score_num") self.t_mod_doron = tex.get_texture("result/score/mod_doron") self.t_mod_abekobe = tex.get_texture("result/score/mod_abekobe") self.t_mod_kimagure = tex.get_texture("result/score/mod_kimagure") self.t_mod_detarame = tex.get_texture("result/score/mod_detarame") self.t_mod_yonbai = tex.get_texture("result/score/mod_yonbai") self.t_mod_sanbai = tex.get_texture("result/score/mod_sanbai") self.t_mod_baisaku = tex.get_texture("result/score/mod_baisaku") self.gauge = Gauge.new(is_2p, false, self.diff_str, { screen = "result", player_str = p, scale = 10.0 / 11.0, footer = true, no_border = true }) self.sc_score_info_margin = 22 * tex.get_screen_scale() local rs = SKIN.result_score_margin self.sc_result_score_margin = rs and rs.x or 0 self.sc_result_2p_offset = -288 * tex.get_screen_scale() return self end function ResultPlayer:update(current_ms, fade_in_finished, score, good, ok, bad, max_combo, total_drumroll) self.fade_in_finished_flag = fade_in_finished self.score = score self.good = good self.ok = ok self.bad = bad self.max_combo = max_combo self.total_drumroll = total_drumroll if fade_in_finished and not self.gauge_started then self.gauge_started = true self.gauge_fade_in:start() self.tamashii_fire:start() self.bottom_chars:start() end if self.gauge_started then if self.gauge_state == 2 then if self.rainbow_start_ms < 0 then self.rainbow_start_ms = current_ms end self.rainbow_frac = math.fmod((current_ms - self.rainbow_start_ms) / 75.0, 8.0) end self.tamashii_fire:update(current_ms) self.gauge_fade_in:update(current_ms) if self.gauge_fade_in.is_finished and self.score_delay == nil then self.score_delay = current_ms + 1883 end end if self.score_delay ~= nil and current_ms > self.score_delay and not self.fade_in_started then self.fade_in_started = true self.fade_in_bottom:start() end self.fade_in_bottom:update(current_ms) if self.fade_in_started then self.bottom_chars:update(current_ms, self.gauge_state) end if self.bottom_chars:is_finished() then if self.gauge_state >= 1 and not self.crown_obj then self.crown_obj = ResultCrown.new(self.crown_type, self.is_2p) if self.crown_type == 1 or self.crown_type == 2 then audio.play_sound("full_combo_voice_" .. tostring(self.player_num) .. "p", "voice") end elseif self.gauge_state == 0 and not self.crown_msg_obj then local frame = (self.gauge_length < 87.0 / 2.0) and 0 or 1 self.crown_msg_obj = ResultCrownMessage.new(frame, self.is_2p) if frame == 0 then if ResultBasin then self.basin_obj = ResultBasin.new(self.is_2p) end audio.play_sound("max_fail_voice_" .. tostring(self.player_num) .. "p", "voice") else audio.play_sound("fail_voice_" .. tostring(self.player_num) .. "p", "voice") end end end if self.crown_obj then self.crown_obj:update(current_ms) if self.crown_obj:is_settled() and not self.crown_msg_obj then local frame = (self.gauge_state == 2) and 3 or 2 self.crown_msg_obj = ResultCrownMessage.new(frame, self.is_2p) if frame == 2 then audio.play_sound("clear_voice_" .. tostring(self.player_num) .. "p", "voice") else audio.play_sound("max_clear_voice_" .. tostring(self.player_num) .. "p", "voice") end end end if self.crown_msg_obj then self.crown_msg_obj:update(current_ms) end if self.basin_obj then self.basin_obj:update(current_ms) self.chara_miss_loop = self.basin_obj.landed -- read by ResultPlayer::update (C++) end if not self.high_score_obj and self.score_diff > 0 and self.good ~= "" then self.high_score_obj = HighScoreIndicator.new(self.score_diff, self.is_2p) end if self.high_score_obj then self.high_score_obj:update(current_ms) end end function ResultPlayer:draw() local p_idx = self.is_2p and 1 or 0 if self.fade_in_started then local fade_val = math.min(0.4, self.fade_in_bottom.attribute) if self.is_2p then if self.gauge_state == 0 then if self.t_grad_fail then tex.draw_texture(self.t_grad_fail, { fade = fade_val }) end else if self.t_grad_clear then tex.draw_texture(self.t_grad_clear, { fade = fade_val }) end end else local y = (self.sc_result_2p_offset ~= 0) and (self.has_2p and 1.0 or 0.0) or 0.0 if self.gauge_state == 0 then if self.t_grad_fail then tex.draw_texture(self.t_grad_fail, { y = y, fade = fade_val }) end else if self.t_grad_clear then tex.draw_texture(self.t_grad_clear, { y = y, fade = fade_val }) end end end end if self.t_overlay then tex.draw_texture(self.t_overlay, { color = { 255, 255, 255, 192 }, index = p_idx }) end if self.t_difficulty then tex.draw_texture(self.t_difficulty, { frame = self.selected_difficulty, index = p_idx }) end if not self.has_2p then self.bottom_chars:draw() end if self.t_judge_good then tex.draw_texture(self.t_judge_good, { index = p_idx }) end if self.t_judge_ok then tex.draw_texture(self.t_judge_ok, { index = p_idx }) end if self.t_judge_bad then tex.draw_texture(self.t_judge_bad, { index = p_idx }) end if self.t_max_combo then tex.draw_texture(self.t_max_combo, { index = p_idx }) end if self.t_drumroll then tex.draw_texture(self.t_drumroll, { index = p_idx }) end self:draw_score_info() self:draw_total_score() if self.crown_obj then self.crown_obj:draw() end if self.crown_msg_obj then self.crown_msg_obj:draw() end self:draw_modifiers() if self.high_score_obj then self.high_score_obj:draw() end end function ResultPlayer:draw_front() if self.basin_obj then self.basin_obj:draw() end end -- Song number, title and subtitle across the top of the screen. info = { title, subtitle, number }; -- subtitle is "" when the chart hides it. local WHITE, BLACK, RED = { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, { 255, 0, 0, 255 } local function cached_text(self, slot, str, size, color, thickness) local key = str .. "\0" .. size local entry = self._song_info_texts[slot] if not entry or entry.key ~= key then entry = { key = key, text = text.create_raw_text(str, size, WHITE, color, false, thickness) } self._song_info_texts[slot] = entry end return entry.text end function ResultPlayer:draw_song_info(info) self._song_info_texts = self._song_info_texts or {} local s = tex.get_screen_scale() local NUM_X, NUM_Y = 232 * s, 15 * s local TITLE_X, TITLE_Y, TITLE_W, TITLE_FONT = 1252 * s, 35 * s, 0, math.floor(40 * s) local SUB_Y, SUB_FONT = 73 * s, math.floor(22 * s) -- "Song {0}": outlined red on the last song of a limited session local num_cfg = tex.get_skin_config("song_num") or { font_size = 0, outline = -1 } local fmt = tex.skin_text("song_num") if fmt == "" then fmt = "{0}" end local limit = tex.song_limit() local num = cached_text(self, "number", fmt:gsub("{0}", tostring(info.number)), num_cfg.font_size, (limit > 0 and limit == info.number) and RED or BLACK, num_cfg.outline >= 0 and num_cfg.outline or 5.0) num:draw({ x = NUM_X, y = NUM_Y, fade = 1.0 }) local title = cached_text(self, "title", info.title, TITLE_FONT, BLACK, 5) local title_x = TITLE_W > 0 and TITLE_X + (TITLE_W - title.width) / 2 or TITLE_X - title.width title:draw({ x = title_x, y = TITLE_Y - title.height / 2, fade = 1.0 }) if info.subtitle ~= "" then local sub = cached_text(self, "subtitle", info.subtitle, SUB_FONT, BLACK, 5) local sub_x = TITLE_W > 0 and TITLE_X + (TITLE_W - sub.width) / 2 or TITLE_X - sub.width sub:draw({ x = sub_x, y = SUB_Y - sub.height / 2, fade = 1.0 }) end end function ResultPlayer:draw_gauge() if not self.gauge_started then return end local clear_points = ({ 6000, 7000, 8000 })[self.art_index + 1] -- easy / normal+hard / oni norma -- gauge_length is 0-87; the epsilon keeps a whole cell from flooring one short local points = self.gauge_length / 87.0 * 10000.0 + 1e-6 self.gauge:draw_result(self.is_2p and 1 or 0, self.gauge_fade_in.attribute, points, 10000, clear_points, self.gauge_state >= 1, self.gauge_state == 2, self.rainbow_frac, math.floor(self.tamashii_fire.attribute)) end function ResultPlayer:draw_score_info() local scores = { self.good, self.ok, self.bad, self.max_combo, self.total_drumroll } local p_idx = self.is_2p and 1 or 0 for j, s in ipairs(scores) do if s ~= "" and self.t_judge_num then local n = math.floor(tonumber(s) or 0) local str = tostring(n) for i = 1, #str do local d = tonumber(string.sub(str, #str - i + 1, #str - i + 1)) or 0 tex.draw_texture(self.t_judge_num, { frame = d, x = -((i - 1) * self.sc_score_info_margin), index = (j - 1) + p_idx * 5, }) end end end end function ResultPlayer:draw_total_score() if not self.fade_in_finished_flag then return end local p_idx = self.is_2p and 1 or 0 if self.timing_text then local info = tex.get_texture_info("score", "score_shinuchi_" .. tex.language()) or tex.get_texture_info("score", "score_shinuchi_ja") if info then self.timing_text:draw({ x = info.x[p_idx + 1], y = info.y[p_idx + 1] }) end end if self.score_method then if self.t_score_shinuchi_lbl then tex.draw_texture(self.t_score_shinuchi_lbl, { index = p_idx }) end else if self.t_score_lbl then tex.draw_texture(self.t_score_lbl, { index = p_idx }) end end if self.score ~= "" and self.t_score_num then local n = math.floor(tonumber(self.score) or 0) local str = tostring(n) for i = 1, #str do local d = tonumber(string.sub(str, #str - i + 1, #str - i + 1)) or 0 tex.draw_texture(self.t_score_num, { frame = d, x = -((i - 1) * self.sc_result_score_margin), index = p_idx, }) end end end function ResultPlayer:draw_modifiers() local p_idx = self.is_2p and 1 or 0 if self.mod_display and self.t_mod_doron then tex.draw_texture(self.t_mod_doron, { index = p_idx }) end if self.mod_inverse and self.t_mod_abekobe then tex.draw_texture(self.t_mod_abekobe, { index = p_idx }) end if self.mod_random == 1 and self.t_mod_kimagure then tex.draw_texture(self.t_mod_kimagure, { index = p_idx }) elseif self.mod_random == 2 and self.t_mod_detarame then tex.draw_texture(self.t_mod_detarame, { index = p_idx }) end if self.mod_speed >= 40 and self.t_mod_yonbai then tex.draw_texture(self.t_mod_yonbai, { index = p_idx }) elseif self.mod_speed >= 30 and self.t_mod_sanbai then tex.draw_texture(self.t_mod_sanbai, { index = p_idx }) elseif self.mod_speed > 10 and self.t_mod_baisaku then tex.draw_texture(self.t_mod_baisaku, { index = p_idx }) end end