diff --git a/assets/mod/autoplay.png b/assets/mod/autoplay.png new file mode 100644 index 000000000..f40a0b0f9 Binary files /dev/null and b/assets/mod/autoplay.png differ diff --git a/assets/mod/fade_in.png b/assets/mod/fade_in.png new file mode 100644 index 000000000..62f357f5b Binary files /dev/null and b/assets/mod/fade_in.png differ diff --git a/assets/mod/fade_out.png b/assets/mod/fade_out.png new file mode 100644 index 000000000..83d1f9e64 Binary files /dev/null and b/assets/mod/fade_out.png differ diff --git a/assets/mod/flip_x.png b/assets/mod/flip_x.png new file mode 100644 index 000000000..f5d0b44f9 Binary files /dev/null and b/assets/mod/flip_x.png differ diff --git a/assets/mod/nightcore.png b/assets/mod/nightcore.png new file mode 100644 index 000000000..284e166f1 Binary files /dev/null and b/assets/mod/nightcore.png differ diff --git a/assets/mod/rainbow.png b/assets/mod/rainbow.png new file mode 100644 index 000000000..90852ad33 Binary files /dev/null and b/assets/mod/rainbow.png differ diff --git a/phira/locales/en-US/song.ftl b/phira/locales/en-US/song.ftl index 1222e8d63..33ed21cb3 100644 --- a/phira/locales/en-US/song.ftl +++ b/phira/locales/en-US/song.ftl @@ -102,11 +102,17 @@ review-edit-tags-done = Tags updated. mods = Mods mods-autoplay = Autoplay -mods-autoplay-sub = Plays the chart without user input. +mods-autoplay-sub = Results will not be submitted when enabled. mods-flip-x = Mirror mods-flip-x-sub = Mirrors the chart by the X-axis. +mods-fade-in = Fade-In +mods-fade-in-sub = Makes notes fade in when they approach the judgeline. mods-fade-out = Fade-Out mods-fade-out-sub = Makes notes fade out when they approach the judgeline. +mods-nightcore = Nightcore +mods-nightcore-sub = Plays the chart at higher speed +mods-rainbow = Rainbow +mods-rainbow-sub = Makes your game a *little* more colorful rate-failed = Rate failed. rate-done = Rated successfully. diff --git a/phira/locales/ja-JP/song.ftl b/phira/locales/ja-JP/song.ftl index 6b6b3805e..236fb45c0 100644 --- a/phira/locales/ja-JP/song.ftl +++ b/phira/locales/ja-JP/song.ftl @@ -54,5 +54,16 @@ ldb = リーダーボード ldb-load-failed = リーダーボードの読み込みに失敗 ldb-no-rank = なし +mods = Mods mods-autoplay = オートプレイ mods-autoplay-sub = これを有効にすると、リザルトの記録が無効になります +mods-flip-x = ミラー +mods-flip-x-sub = X軸に沿って譜面を反転させます +mods-fade-in = フェードイン +mods-fade-in-sub = ノーツが判定線に近づくと現れるようにします +mods-fade-out = フェードアウト +mods-fade-out-sub = ノーツが判定線に近づくと消えるようにします +mods-nightcore = ナイトコア +mods-nightcore-sub = より高速で譜面をプレイします +mods-rainbow = レインボー +mods-rainbow-sub = ゲームを *少しだけ* カラフルにします diff --git a/phira/locales/zh-CN/song.ftl b/phira/locales/zh-CN/song.ftl index b8187ca71..306d4ce73 100644 --- a/phira/locales/zh-CN/song.ftl +++ b/phira/locales/zh-CN/song.ftl @@ -103,8 +103,14 @@ mods-autoplay = 自动游玩 mods-autoplay-sub = 启用后将无法上传成绩 mods-flip-x = X 轴反转 mods-flip-x-sub = 在 X 轴上反转谱面 +mods-fade-in = 上隐 +mods-fade-in-sub = 音符在靠近判定线时会显现 mods-fade-out = 下隐 mods-fade-out-sub = 音符在靠近判定线时会隐藏 +mods-nightcore = 夜店 +mods-nightcore-sub = 以高倍速游玩谱面 +mods-rainbow = 彩虹 +mods-rainbow-sub = 遇上彩虹,吃定彩虹 rate-failed = 评分失败 rate-done = 评分成功 diff --git a/phira/src/scene/song.rs b/phira/src/scene/song.rs index 89a50f16e..663d67984 100644 --- a/phira/src/scene/song.rs +++ b/phira/src/scene/song.rs @@ -1267,7 +1267,7 @@ impl SongScene { let (btn, clicked) = &mut self.mod_btns[index]; if *clicked { *clicked = false; - self.mods.toggle(flag); + self.mods.toggle_mod(flag); } let on = self.mods.contains(flag); let oh = rr.h; @@ -1288,7 +1288,10 @@ impl SongScene { }; item(tl!("mods-autoplay"), Some(tl!("mods-autoplay-sub")), Mods::AUTOPLAY); item(tl!("mods-flip-x"), Some(tl!("mods-flip-x-sub")), Mods::FLIP_X); + item(tl!("mods-fade-in"), Some(tl!("mods-fade-in-sub")), Mods::FADE_IN); item(tl!("mods-fade-out"), Some(tl!("mods-fade-out-sub")), Mods::FADE_OUT); + item(tl!("mods-nightcore"), Some(tl!("mods-nightcore-sub")), Mods::NIGHTCORE); + item(tl!("mods-rainbow"), Some(tl!("mods-rainbow-sub")), Mods::RAINBOW); (width, h) }); } diff --git a/prpr/src/config.rs b/prpr/src/config.rs index 1e5670af3..8b5a8147c 100644 --- a/prpr/src/config.rs +++ b/prpr/src/config.rs @@ -14,6 +14,29 @@ bitflags! { const AUTOPLAY = 1; const FLIP_X = 2; const FADE_OUT = 4; + const FADE_IN = 8; + const NIGHTCORE = 16; + const RAINBOW = 32; + } +} + +impl Mods { + pub fn toggle_mod(&mut self, flag: Mods) { + if self.contains(flag) { + self.remove(flag); + } else { + for &conflict in Mods::conflicts(flag) { + self.remove(conflict); + } + self.insert(flag); + } + } + fn conflicts(flag: Mods) -> &'static [Mods] { + match flag { + Mods::FADE_IN => &[Mods::FADE_OUT], + Mods::FADE_OUT => &[Mods::FADE_IN], + _ => &[], + } } } diff --git a/prpr/src/core/line.rs b/prpr/src/core/line.rs index f51cca6f6..f5f3a18c8 100644 --- a/prpr/src/core/line.rs +++ b/prpr/src/core/line.rs @@ -1,8 +1,7 @@ use super::{chart::ChartSettings, object::CtrlObject, Anim, AnimFloat, BpmList, Matrix, Note, Object, Point, RenderConfig, Resource, Vector}; use crate::{ - config::Mods, ext::{get_viewport, NotNanExt, SafeTexture}, - judge::{JudgeStatus, LIMIT_BAD}, + judge::JudgeStatus, ui::Ui, }; use macroquad::prelude::*; @@ -359,13 +358,9 @@ impl JudgeLine { ctrl_obj: &mut self.ctrl_obj.borrow_mut(), line_height: self.height.now(), appear_before: f32::INFINITY, - invisible_time: f32::INFINITY, draw_below: self.show_below, incline_sin: self.incline.now_opt().map(|it| it.to_radians().sin()).unwrap_or_default(), }; - if res.config.has_mod(Mods::FADE_OUT) { - config.invisible_time = LIMIT_BAD; - } if alpha < 0.0 { if !settings.pe_alpha_extension { return; diff --git a/prpr/src/core/note.rs b/prpr/src/core/note.rs index 27b722c27..b49b6cbd1 100644 --- a/prpr/src/core/note.rs +++ b/prpr/src/core/note.rs @@ -1,6 +1,7 @@ use super::{chart::ChartSettings, BpmList, CtrlObject, JudgeLine, Matrix, Object, Point, Resource}; pub use crate::{ - judge::{HitSound, JudgeStatus}, + config::Mods, + judge::{HitSound, JudgeStatus, LIMIT_BAD}, parse::RPE_HEIGHT, }; use macroquad::prelude::*; @@ -48,7 +49,6 @@ pub struct RenderConfig<'a> { pub ctrl_obj: &'a mut CtrlObject, pub line_height: f32, pub appear_before: f32, - pub invisible_time: f32, pub draw_below: bool, pub incline_sin: f32, } @@ -198,9 +198,6 @@ impl Note { return; } } - if config.invisible_time.is_finite() && self.time - config.invisible_time < res.time { - return; - } let scale = (if res.config.double_hint && self.multiple_hint { res.res_pack.note_style_mh.click.width() / res.res_pack.note_style.click.width() } else { @@ -240,11 +237,19 @@ impl Note { } else { &res.res_pack.note_style }; + let mod_alpha = if res.config.has_mod(Mods::FADE_OUT) { + ((self.time - res.time - LIMIT_BAD) / LIMIT_BAD).clamp(0., 1.) + } else if res.config.has_mod(Mods::FADE_IN) { + (1. - (self.time - res.time - LIMIT_BAD) / LIMIT_BAD).clamp(0., 1.) + } else { + 1. + }; let draw = |res: &mut Resource, tex: Texture2D| { let mut color = color; if !config.draw_below { color.a *= (self.time - res.time).min(0.) / FADEOUT_TIME + 1.; } + color.a *= mod_alpha; res.with_model(self.now_transform(res, ctrl_obj, base, config.incline_sin), |res| { draw_center(res, tex, order, scale, color); }); @@ -268,6 +273,7 @@ impl Note { return; } let end_height = end_height / res.aspect_ratio * spd; + color.a *= mod_alpha; let h = if self.time <= res.time { line_height } else { height }; let bottom = h - line_height; diff --git a/prpr/src/core/resource.rs b/prpr/src/core/resource.rs index 0bec5013b..04a9a765e 100644 --- a/prpr/src/core/resource.rs +++ b/prpr/src/core/resource.rs @@ -389,6 +389,7 @@ pub struct Resource { pub background: SafeTexture, pub illustration: SafeTexture, pub icons: [SafeTexture; 8], + pub mod_icons: [SafeTexture; 6], pub res_pack: ResourcePack, pub player: SafeTexture, pub icon_back: SafeTexture, @@ -415,17 +416,18 @@ pub struct Resource { pub model_stack: Vec, } +macro_rules! loads { + ($($path:literal),*) => { + [$(loads!(@detail $path)),*] + }; + + (@detail $path:literal) => { + Texture2D::from_image(&load_image($path).await?).into() + }; +} + impl Resource { pub async fn load_icons() -> Result<[SafeTexture; 8]> { - macro_rules! loads { - ($($path:literal),*) => { - [$(loads!(@detail $path)),*] - }; - - (@detail $path:literal) => { - Texture2D::from_image(&load_image($path).await?).into() - }; - } Ok(loads![ "rank/F.png", "rank/C.png", @@ -437,6 +439,17 @@ impl Resource { "rank/phi.png" ]) } + pub async fn load_mod_icons() -> Result<[SafeTexture; 6]> { + // FLIP_X, FADE_OUT, FADE_IN, NIGHTCORE, RAINBOW, AUTOPLAY + Ok(loads![ + "mod/flip_x.png", + "mod/fade_out.png", + "mod/fade_in.png", + "mod/nightcore.png", + "mod/rainbow.png", + "mod/autoplay.png" + ]) + } pub async fn new( config: Config, @@ -496,6 +509,7 @@ impl Resource { background, illustration, icons: Self::load_icons().await?, + mod_icons: Self::load_mod_icons().await?, res_pack, player: if let Some(player) = player { player } else { load_tex!("player.jpg") }, icon_back: load_tex!("back.png"), diff --git a/prpr/src/scene/ending.rs b/prpr/src/scene/ending.rs index 6cdd46876..1e2038e95 100644 --- a/prpr/src/scene/ending.rs +++ b/prpr/src/scene/ending.rs @@ -2,7 +2,7 @@ prpr_l10n::tl_file!("ending"); use super::{draw_background, game::SimpleRecord, loading::UploadFn, NextScene, Scene}; use crate::{ - config::Config, + config::{Config, Mods}, core::{BOLD_FONT, PGR_FONT}, ext::{create_audio_manger, rect_shadow, semi_black, semi_white, RectExt, SafeTexture, ScaleType}, info::ChartInfo, @@ -33,6 +33,7 @@ pub struct EndingScene { icons: [SafeTexture; 8], icon_retry: SafeTexture, icon_proceed: SafeTexture, + mod_icons: [SafeTexture; 6], target: Option, audio: AudioManager, bgm: Music, @@ -43,6 +44,7 @@ pub struct EndingScene { player_rks: Option, autoplay: bool, speed: f32, + mods: Mods, next: u8, // 0 -> none, 1 -> pop, 2 -> exit update_state: Option, rated: bool, @@ -71,6 +73,7 @@ impl EndingScene { icons: [SafeTexture; 8], icon_retry: SafeTexture, icon_proceed: SafeTexture, + mod_icons: [SafeTexture; 6], info: ChartInfo, result: PlayResult, config: &Config, @@ -101,6 +104,7 @@ impl EndingScene { icons, icon_retry, icon_proceed, + mod_icons, target: None, audio, bgm, @@ -127,6 +131,7 @@ impl EndingScene { player_rks, autoplay: config.autoplay(), speed: config.speed, + mods: config.mods, next: 0, upload_fn, @@ -536,37 +541,83 @@ impl Scene for EndingScene { } else { format!("{:.2}x", self.speed) }; - let text = if self.autoplay { - format!("AUTOPLAY {spd}") - } else if !self.rated { - format!("UNRATED {spd}") + let status_text = if !self.rated && !self.autoplay { + if spd.is_empty() { + "UNRATED".to_string() + } else { + format!("UNRATED {spd}") + } } else { spd }; - let text = text.trim(); - if !text.is_empty() { - let ty = br.bottom(); - let x = -0.55 + (1.2 - ty) / 1.9 * 0.4; - let h = 0.04; - let mut text = ui - .text(text) - .pos(x + 0.02, ty - h / 2.) - .anchor(0., 0.5) - .no_baseline() - .color(semi_black(0.6)) - .size(0.5); - let tr = text.measure_using(&BOLD_FONT); - let r = Rect::new(-1., tr.y, tr.right() + 1.03, tr.h); - let mut b = text.ui.builder(WHITE); - b.add(-1., tr.y); - b.add(r.right(), tr.y); - b.add(r.right() - tr.h / 1.9 * 0.4, tr.bottom()); - b.add(-1., tr.bottom()); - b.triangle(0, 1, 2); - b.triangle(0, 2, 3); - b.commit(); + let status_text = status_text.trim(); + // mod_icons order: FLIP_X, FADE_OUT, FADE_IN, NIGHTCORE, RAINBOW + let active_mod_indices: Vec = [ + (Mods::FLIP_X, 0), + (Mods::FADE_OUT, 1), + (Mods::FADE_IN, 2), + (Mods::NIGHTCORE, 3), + (Mods::RAINBOW, 4), + (Mods::AUTOPLAY, 5), + ] + .into_iter() + .filter(|(m, _)| self.mods.contains(*m)) + .map(|(_, idx)| idx) + .collect(); + let ty = br.bottom(); + let base_x = -0.55 + (1.2 - ty) / 1.9 * 0.4; + let skew_factor = 0.4 / 1.9; + let has_text = !status_text.is_empty(); + let has_icons = !active_mod_indices.is_empty(); + if has_text || has_icons { + let text_size = 0.5; + let skew_height_ratio = skew_factor; + let mut current_x = base_x; + let para_h = 0.04; + if has_text { + let mut text = ui + .text(status_text) + .pos(current_x + 0.02, ty) + .anchor(0., 0.5) + .no_baseline() + .color(semi_black(0.6)) + .size(text_size); + let tr = text.measure_using(&BOLD_FONT); + let r = Rect::new(-1., tr.y, tr.right() + 1.03, tr.h); + let mut b = text.ui.builder(WHITE); + b.add(-1., tr.y); + b.add(r.right(), tr.y); + b.add(r.right() - tr.h * skew_height_ratio, tr.bottom()); + b.add(-1., tr.bottom()); + b.triangle(0, 1, 2); + b.triangle(0, 2, 3); + b.commit(); - text.draw_using(&BOLD_FONT); + text.draw_using(&BOLD_FONT); + current_x = tr.right() + 0.04; + } + for &mod_idx in &active_mod_indices { + let icon_size = para_h * 0.9; + let para_w = para_h + 0.02; + let skew_offset = para_h * skew_height_ratio; + let para_left = current_x; + let para_right = current_x + para_w; + let para_top = ty - para_h / 2.; + let para_bottom = ty + para_h / 2.; + let mut b = ui.builder(WHITE); + b.add(para_left + skew_offset, para_top); + b.add(para_right + skew_offset, para_top); + b.add(para_right, para_bottom); + b.add(para_left, para_bottom); + b.triangle(0, 1, 2); + b.triangle(0, 2, 3); + b.commit(); + let icon_x = current_x + (para_w - icon_size) / 2. + skew_offset / 2.; + let icon_y = ty - icon_size / 2.; + let icon_rect = Rect::new(icon_x, icon_y, icon_size, icon_size); + ui.fill_rect(icon_rect, (*self.mod_icons[mod_idx], icon_rect, ScaleType::Fit, semi_black(0.6))); + current_x = para_right + 0.02; + } } } clip_sector(ui, ct, sector_start, sector_start + center_angle, |ui| { diff --git a/prpr/src/scene/game.rs b/prpr/src/scene/game.rs index 8dd2746b5..9cb8d355b 100644 --- a/prpr/src/scene/game.rs +++ b/prpr/src/scene/game.rs @@ -262,6 +262,17 @@ impl GameScene { .push(Effect::new(0.0..f32::INFINITY, include_str!("fxaa.glsl"), Vec::new(), false).unwrap()); } + if config.has_mod(Mods::NIGHTCORE) { + config.speed *= 1.5; + } + + if config.has_mod(Mods::RAINBOW) { + chart + .extra + .effects + .push(Effect::new(0.0..f32::INFINITY, include_str!("rainbow.glsl"), Vec::new(), false).unwrap()); + } + let info_offset = info.offset; let mut res = Resource::new( config, @@ -965,6 +976,7 @@ impl Scene for GameScene { self.res.icons.clone(), self.res.icon_retry.clone(), self.res.icon_proceed.clone(), + self.res.mod_icons.clone(), self.res.info.clone(), self.judge.result(), &self.res.config, diff --git a/prpr/src/scene/rainbow.glsl b/prpr/src/scene/rainbow.glsl new file mode 100644 index 000000000..cf2eea069 --- /dev/null +++ b/prpr/src/scene/rainbow.glsl @@ -0,0 +1,37 @@ +#version 100 +precision highp float; + +varying lowp vec2 uv; +uniform sampler2D screenTexture; +uniform float time; + +const float hueSpeed = 0.5; + +vec3 rgb2hsv(vec3 c) { + vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), + d / (q.x + e), + q.x); +} + +vec3 hsv2rgb(vec3 c) { + vec3 p = abs(fract(c.xxx + vec3(0.0, 2.0/3.0, 1.0/3.0)) * 6.0 - 3.0); + return c.z * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), c.y); +} + +void main() { + vec4 color = texture2D(screenTexture, uv); + + vec3 hsv = rgb2hsv(color.rgb); + + hsv.x = fract(hsv.x + time * hueSpeed); + + vec3 rgb = hsv2rgb(hsv); + + gl_FragColor = vec4(rgb, color.a); +}