From a6bed95de36ed41354a8794d824c29af27c19ffa Mon Sep 17 00:00:00 2001 From: asimon-1 <40246417+asimon-1@users.noreply.github.com> Date: Sat, 9 Apr 2022 18:10:44 -0400 Subject: [PATCH] Tabbed Web Menu (#333) * Web menu refactor * Fix some menu items * Fixes for quick_menu, general clippy fixes * Revert small testing change * Add quick menu SVG * Fix defaults saving/loading * Log the last URL from the web menu Co-authored-by: jugeeya <jugeeya@live.com> --- src/common/menu.rs | 44 +- src/common/mod.rs | 276 ++++---- src/lib.rs | 35 +- src/static/css/training_modpack.css | 466 ++++++------- src/static/img/quick_menu.svg | 29 + src/static/js/training_modpack.js | 399 ++++++------ src/templates/menu.html | 160 ++--- training_mod_consts/src/lib.rs | 974 ++++++++++++---------------- training_mod_tui/src/lib.rs | 163 ++--- training_mod_tui/src/list.rs | 53 +- training_mod_tui/src/main.rs | 4 +- 11 files changed, 1137 insertions(+), 1466 deletions(-) create mode 100644 src/static/img/quick_menu.svg diff --git a/src/common/menu.rs b/src/common/menu.rs index ee8f787..bc074e6 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -66,10 +66,10 @@ pub unsafe fn write_menu() { const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.conf"; -pub fn set_menu_from_url(orig_last_url: &str) { - let last_url = &orig_last_url.replace("&save_defaults=1", ""); +pub fn set_menu_from_url(last_url: &str) { unsafe { - MENU = get_menu_from_url(MENU, last_url); + MENU = get_menu_from_url(MENU, last_url, false); + DEFAULTS_MENU = get_menu_from_url(MENU, last_url, true); if MENU.quick_menu == OnOff::Off { if is_emulator() { @@ -83,17 +83,6 @@ pub fn set_menu_from_url(orig_last_url: &str) { } } - if last_url.len() != orig_last_url.len() { - // Save as default - unsafe { - DEFAULT_MENU = MENU; - write_menu(); - } - let menu_defaults_conf_path = "sd:/TrainingModpack/training_modpack_menu_defaults.conf"; - std::fs::write(menu_defaults_conf_path, last_url) - .expect("Failed to write default menu conf file"); - } - std::fs::write(MENU_CONF_PATH, last_url).expect("Failed to write menu conf file"); unsafe { EVENT_QUEUE.push(Event::menu_open(last_url.to_string())); @@ -115,19 +104,22 @@ pub fn spawn_menu() { if !quick_menu { let fname = "training_menu.html"; - let params = unsafe { MENU.to_url_params() }; - let page_response = Webpage::new() - .background(Background::BlurredScreenshot) - .htdocs_dir("training_modpack") - .boot_display(BootDisplay::BlurredScreenshot) - .boot_icon(true) - .start_page(&format!("{}{}", fname, params)) - .open() - .unwrap(); + unsafe { + let params = MENU.to_url_params(false); + let default_params = DEFAULTS_MENU.to_url_params(true); + let page_response = Webpage::new() + .background(Background::BlurredScreenshot) + .htdocs_dir("training_modpack") + .boot_display(BootDisplay::BlurredScreenshot) + .boot_icon(true) + .start_page(&format!("{}?{}&{}", fname, params, default_params)) + .open() + .unwrap(); - let orig_last_url = page_response.get_last_url().unwrap(); - - set_menu_from_url(orig_last_url); + let last_url = page_response.get_last_url().unwrap(); + println!("Received URL from web menu: {}", last_url); + set_menu_from_url(last_url); + } } else { unsafe { QUICK_MENU_ACTIVE = true; diff --git a/src/common/mod.rs b/src/common/mod.rs index 1673bc5..be0c7ea 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,138 +1,138 @@ -pub mod consts; -pub mod events; -pub mod menu; -pub mod raygun_printer; -pub mod release; - -use crate::common::consts::*; -use smash::app::{self, lua_bind::*}; -use smash::lib::lua_const::*; - -pub use crate::common::consts::MENU; -pub static mut DEFAULT_MENU: TrainingModpackMenu = crate::common::consts::DEFAULT_MENU; -pub static mut BASE_MENU: TrainingModpackMenu = unsafe { DEFAULT_MENU }; -pub static mut FIGHTER_MANAGER_ADDR: usize = 0; -pub static mut STAGE_MANAGER_ADDR: usize = 0; - -#[cfg(not(feature = "outside_training_mode"))] -extern "C" { - #[link_name = "\u{1}_ZN3app9smashball16is_training_modeEv"] - pub fn is_training_mode() -> bool; -} - -#[cfg(feature = "outside_training_mode")] -pub fn is_training_mode() -> bool { - return true; -} - -pub fn get_category(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 { - (module_accessor.info >> 28) as u8 as i32 -} - -pub fn is_emulator() -> bool { - unsafe { skyline::hooks::getRegionAddress(skyline::hooks::Region::Text) as u64 == 0x8004000 } -} - -pub fn get_module_accessor(fighter_id: FighterId) -> *mut app::BattleObjectModuleAccessor { - let entry_id_int = fighter_id as i32; - let entry_id = app::FighterEntryID(entry_id_int); - unsafe { - let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); - let fighter_entry = - FighterManager::get_fighter_entry(mgr, entry_id) as *mut app::FighterEntry; - let current_fighter_id = FighterEntry::current_fighter_id(fighter_entry); - app::sv_battle_object::module_accessor(current_fighter_id as u32) - } -} - -pub fn is_fighter(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - get_category(module_accessor) == BATTLE_OBJECT_CATEGORY_FIGHTER -} - -pub fn is_operation_cpu(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - unsafe { - if !is_fighter(module_accessor) { - return false; - } - - let entry_id_int = - WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID) as i32; - - if entry_id_int == 0 { - return false; - } - - let entry_id = app::FighterEntryID(entry_id_int); - let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); - let fighter_information = - FighterManager::get_fighter_information(mgr, entry_id) as *mut app::FighterInformation; - - FighterInformation::is_operation_cpu(fighter_information) - } -} - -pub fn is_grounded(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; - - situation_kind == SITUATION_KIND_GROUND -} - -pub fn is_airborne(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; - - situation_kind == SITUATION_KIND_AIR -} - -pub fn is_idle(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - - status_kind == FIGHTER_STATUS_KIND_WAIT -} - -pub fn is_in_hitstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - - (*FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_FALL).contains(&status_kind) -} -pub fn is_in_footstool(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - - (*FIGHTER_STATUS_KIND_TREAD_DAMAGE..=*FIGHTER_STATUS_KIND_TREAD_FALL).contains(&status_kind) -} - -pub fn is_shielding(module_accessor: *mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) as i32 }; - - (*FIGHTER_STATUS_KIND_GUARD_ON..=*FIGHTER_STATUS_KIND_GUARD_DAMAGE).contains(&status_kind) -} - -pub fn is_in_shieldstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - let prev_status = unsafe { StatusModule::prev_status_kind(module_accessor, 0) }; - - // If we are taking shield damage or we are droping shield from taking shield damage we are in hitstun - status_kind == FIGHTER_STATUS_KIND_GUARD_DAMAGE - || (prev_status == FIGHTER_STATUS_KIND_GUARD_DAMAGE - && status_kind == FIGHTER_STATUS_KIND_GUARD_OFF) -} - -pub unsafe fn is_dead(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let fighter_kind = app::utility::get_kind(module_accessor); - let fighter_is_ptrainer = [ - *FIGHTER_KIND_PZENIGAME, - *FIGHTER_KIND_PFUSHIGISOU, - *FIGHTER_KIND_PLIZARDON, - ] - .contains(&fighter_kind); - let status_kind = StatusModule::status_kind(module_accessor) as i32; - let prev_status_kind = StatusModule::prev_status_kind(module_accessor, 0); - // Pokemon trainer enters FIGHTER_STATUS_KIND_WAIT for one frame during their respawn animation - // And the previous status is FIGHTER_STATUS_NONE - if fighter_is_ptrainer { - [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) - || (status_kind == FIGHTER_STATUS_KIND_WAIT - && prev_status_kind == FIGHTER_STATUS_KIND_NONE) - } else { - [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) - } -} +pub mod consts; +pub mod events; +pub mod menu; +pub mod raygun_printer; +pub mod release; + +use crate::common::consts::*; +use smash::app::{self, lua_bind::*}; +use smash::lib::lua_const::*; + +pub use crate::common::consts::MENU; +pub static mut DEFAULTS_MENU: TrainingModpackMenu = crate::common::consts::DEFAULTS_MENU; +pub static mut BASE_MENU: TrainingModpackMenu = unsafe { DEFAULTS_MENU }; +pub static mut FIGHTER_MANAGER_ADDR: usize = 0; +pub static mut STAGE_MANAGER_ADDR: usize = 0; + +#[cfg(not(feature = "outside_training_mode"))] +extern "C" { + #[link_name = "\u{1}_ZN3app9smashball16is_training_modeEv"] + pub fn is_training_mode() -> bool; +} + +#[cfg(feature = "outside_training_mode")] +pub fn is_training_mode() -> bool { + return true; +} + +pub fn get_category(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 { + (module_accessor.info >> 28) as u8 as i32 +} + +pub fn is_emulator() -> bool { + unsafe { skyline::hooks::getRegionAddress(skyline::hooks::Region::Text) as u64 == 0x8004000 } +} + +pub fn get_module_accessor(fighter_id: FighterId) -> *mut app::BattleObjectModuleAccessor { + let entry_id_int = fighter_id as i32; + let entry_id = app::FighterEntryID(entry_id_int); + unsafe { + let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); + let fighter_entry = + FighterManager::get_fighter_entry(mgr, entry_id) as *mut app::FighterEntry; + let current_fighter_id = FighterEntry::current_fighter_id(fighter_entry); + app::sv_battle_object::module_accessor(current_fighter_id as u32) + } +} + +pub fn is_fighter(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + get_category(module_accessor) == BATTLE_OBJECT_CATEGORY_FIGHTER +} + +pub fn is_operation_cpu(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + unsafe { + if !is_fighter(module_accessor) { + return false; + } + + let entry_id_int = + WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID) as i32; + + if entry_id_int == 0 { + return false; + } + + let entry_id = app::FighterEntryID(entry_id_int); + let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); + let fighter_information = + FighterManager::get_fighter_information(mgr, entry_id) as *mut app::FighterInformation; + + FighterInformation::is_operation_cpu(fighter_information) + } +} + +pub fn is_grounded(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; + + situation_kind == SITUATION_KIND_GROUND +} + +pub fn is_airborne(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; + + situation_kind == SITUATION_KIND_AIR +} + +pub fn is_idle(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + + status_kind == FIGHTER_STATUS_KIND_WAIT +} + +pub fn is_in_hitstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + + (*FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_FALL).contains(&status_kind) +} +pub fn is_in_footstool(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + + (*FIGHTER_STATUS_KIND_TREAD_DAMAGE..=*FIGHTER_STATUS_KIND_TREAD_FALL).contains(&status_kind) +} + +pub fn is_shielding(module_accessor: *mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) as i32 }; + + (*FIGHTER_STATUS_KIND_GUARD_ON..=*FIGHTER_STATUS_KIND_GUARD_DAMAGE).contains(&status_kind) +} + +pub fn is_in_shieldstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + let prev_status = unsafe { StatusModule::prev_status_kind(module_accessor, 0) }; + + // If we are taking shield damage or we are droping shield from taking shield damage we are in hitstun + status_kind == FIGHTER_STATUS_KIND_GUARD_DAMAGE + || (prev_status == FIGHTER_STATUS_KIND_GUARD_DAMAGE + && status_kind == FIGHTER_STATUS_KIND_GUARD_OFF) +} + +pub unsafe fn is_dead(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let fighter_kind = app::utility::get_kind(module_accessor); + let fighter_is_ptrainer = [ + *FIGHTER_KIND_PZENIGAME, + *FIGHTER_KIND_PFUSHIGISOU, + *FIGHTER_KIND_PLIZARDON, + ] + .contains(&fighter_kind); + let status_kind = StatusModule::status_kind(module_accessor) as i32; + let prev_status_kind = StatusModule::prev_status_kind(module_accessor, 0); + // Pokemon trainer enters FIGHTER_STATUS_KIND_WAIT for one frame during their respawn animation + // And the previous status is FIGHTER_STATUS_NONE + if fighter_is_ptrainer { + [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) + || (status_kind == FIGHTER_STATUS_KIND_WAIT + && prev_status_kind == FIGHTER_STATUS_KIND_NONE) + } else { + [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) + } +} diff --git a/src/lib.rs b/src/lib.rs index d110a66..9fa5a17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,7 @@ pub fn render_text_to_screen(s: &str) { pub fn main() { macro_rules! log { ($($arg:tt)*) => { - print!("{}{}", "[Training Modpack] ".green(), format!($($arg)*)); + println!("{}{}", "[Training Modpack] ".green(), format!($($arg)*)); }; } @@ -105,10 +105,8 @@ pub fn main() { if menu_conf.starts_with(b"http://localhost") { log!("Previous menu found, loading from training_modpack_menu.conf"); unsafe { - MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap()); - if is_emulator() { - MENU.quick_menu = OnOff::On; - } + MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap(), false); + DEFAULTS_MENU = get_menu_from_url(DEFAULTS_MENU, std::str::from_utf8(&menu_conf).unwrap(), true); } } else { log!("Previous menu found but is invalid."); @@ -116,32 +114,10 @@ pub fn main() { } else { log!("No previous menu file found."); } - - let menu_defaults_conf_path = "sd:/TrainingModpack/training_modpack_menu_defaults.conf"; - log!("Checking for previous menu defaults in training_modpack_menu_defaults.conf..."); - if fs::metadata(menu_defaults_conf_path).is_ok() { - let menu_defaults_conf = fs::read(menu_defaults_conf_path).unwrap(); - if menu_defaults_conf.starts_with(b"http://localhost") { - log!("Menu defaults found, loading from training_modpack_menu_defaults.conf"); - unsafe { - DEFAULT_MENU = get_menu_from_url( - DEFAULT_MENU, - std::str::from_utf8(&menu_defaults_conf).unwrap(), - ); - if is_emulator() { - DEFAULT_MENU.quick_menu = OnOff::On; - } - crate::menu::write_menu(); - } - } else { - log!("Previous menu defaults found but are invalid."); - } - } else { - log!("No previous menu defaults found."); - } - + if is_emulator() { unsafe { + DEFAULTS_MENU.quick_menu = OnOff::On; MENU.quick_menu = OnOff::On; } } @@ -195,6 +171,7 @@ pub fn main() { // Leave menu. menu::QUICK_MENU_ACTIVE = false; crate::menu::set_menu_from_url(url.as_str()); + println!("URL: {}", url.as_str()); } }); button_presses.zl.read_press().then(|| { diff --git a/src/static/css/training_modpack.css b/src/static/css/training_modpack.css index aa57bb5..46286ae 100644 --- a/src/static/css/training_modpack.css +++ b/src/static/css/training_modpack.css @@ -20,337 +20,261 @@ } } -.answer-border-outer { - margin-top: 5px; -} - -.button-icon { - height: 53px; - margin-left: 4px; - width: 53px; -} - -.button-icon-wrapper { - align-items: center; - background: #000000; +.tab-list-container { + overflow: hidden; + background-color: #555; display: flex; - height: 58px; - margin-left: -1px; - width: 80px; + justify-content: flex-start; + width: 100%; + align-items: center; } -.button-msg-wrapper { - width: 259px; +.tab-list-container p { + color: #fff; + width: 130px; + height: fit-content; + margin: 0px 10px 0px 10px; + padding: 0px; } -.checkbox-display { - margin: 10px 70px; -} -.checkbox-display::after { - /* Displayed Checkbox (unchecked) */ - color: white; - content: "\E14C"; -} - -.defaults-checkbox-container { - /* Save Defaults Container */ +.tab-list { + overflow: hidden; display: flex; - flex-direction: column; - justify-content: center; - margin-top: 10px; - position: fixed; - right: 50px; + justify-content: flex-start; + width: 100%; } -.flex-button { - align-items: center; - display: inline-flex; - flex: 1; - justify-content: center; - text-align: center; - vertical-align: middle; +.tab-list button { + background-color: inherit; + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 14px 16px; + color: #fff; + margin: 5px 0px 0px 8px; + border-radius: 8px 8px 0px 0px; + font-size: large; } -.footer { - align-items: center; - background: #000000; +.tab-list button:hover { + background: #797979; +} + +.tab-list button.active { + color: #000; + background: #ccc; +} + +.tab-content { + width: 100%; display: flex; - height: 73px; - justify-content: center; - position: fixed; - z-index: 10; + flex-wrap: wrap; + justify-content: space-evenly; } -.header { - align-items: center; - background: #000000; - border-bottom: 7px solid #000000; +body { + background: none; + font-family: "nintendo_ext_003", "nintendo_udsgr_std_003"; + margin: 0; +} + +/* Overwrite padding from keyword stuff. */ +.l-main-content { + padding: 0px 0px 0px; +} + +/* Handle alignment of items in the header */ +.l-header { display: flex; - height: 65px; -} - -.header-decoration { - align-items: center; - background: #a80114; - display: inline-flex; - height: 65px; - padding-left: 21px; - width: 101px; -} - -.header-desc { - color: white; + justify-content: space-between; + position: relative; + width: 100%; + height: 80px; + z-index: 100; + background: #000; + box-shadow: 0px 1px 1px #000; } .header-title { color: #f46264; font-size: 26px; line-height: 2.5; + margin: 0px; + padding: 0px; + word-break: normal; } -.is-appear { - opacity: 1; +.header-label { + display: flex; + flex-direction: column; + justify-content: center; + align-items: end; + margin-right: 15px; } -.is-focused .question-border::before { - background: #000000; - box-shadow: none; +.header-label > p { + color: #fff; + margin: 0; + padding: 0; } -.is-focused .question-message { - color: #FFFFFF; - text-shadow: 2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000, 1px 1px #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000; +.return-icon-container { + width: 101px; + height: 65px; + padding-left: 21px; + background: #a80114; + border-radius: 0px 0px 15px 0px; } -.is-focused .question.scuffle-thema { - background: #df1624; +.return-icon { + width: 58px; + height: 58px; + padding-left: 7px; + filter: drop-shadow(3px 5px 2px rgba(0, 0, 0, 0.8)); } -.is-focused .scuffle-thema { - background: #df1624; +/* Center Icons */ +.question::before { + width: 70px; } -.is-hidden { - display: none; -} - -.is-opened .question { - bottom: 11px; -} - -.is-opened .question-border::before { - background: #000000; - bottom: 5px; -} - -.is-opened .question-outer { - height: 86px; - width: 100%; -} - -.keyword-button { - background: #d9e4ea; - border: solid 4px #2e3c45; - box-shadow: 1px 1px 6px rgba(24, 24, 24, 0.6); - box-sizing: border-box; - height: 66px; - justify-content: flex-start; - position: relative; - z-index: 0; -} - -.keyword-button-outer { - border: 5px solid transparent; - will-change: transform; -} - -.keyword-message { - color: #2b3940; - font-size: 22px; - padding: 0px 5px; -} - -.l-footer { +/* Footer */ +.footer { + position: fixed; bottom: 0px; left: 0px; - width: 100%; -} - -.l-grid { + background: #000; display: flex; - flex-wrap: wrap; -} - -.l-header { - box-shadow: 0px 1px 1px #000000; - display: flex; - left: 0px; - position: absolute; - top: 0px; + justify-content: center; + align-items: center; + height: 73px; width: 100%; + color: #fff; z-index: 100; } -.l-header-title { - align-items: center; +/* Save Defaults Container */ +.defaults-checkbox-container { + position: fixed; + right: 50px; + margin-top: 10px; display: flex; - height: 100%; justify-content: center; - left: 0px; - position: absolute; - top: 0px; - width: 1280px; -} - -.l-item { - margin: 0px 13px; -} - -.l-main-content { - /* Overwrite padding from keyword stuff. */ - padding: 0px 0px 0px; -} - -.l-qa { - /* Column size */ - flex-basis: 33%; -} - -.l-qa:last-child .qa { - /* Overwrite margin on the last child to avoid overlap with footer */ - margin-bottom: 75px; -} - -.l-qa:last-child .qa.is-opened { - margin-bottom: 0px; -} - -.qa { - display: block; - will-change: scroll-position; -} - -.is-focused { - background: linear-gradient(90deg, rgb(255, 109, 0) 0%, rgb(255, 255, 0) 65%, rgb(255, 109, 0) 70%); - will-change: animation; - animation: background-slide 650ms linear infinite normal; -} - -.question { - align-items: center; - background: #d9e4ea; - bottom: 11px; - display: flex; - left: 11px; - padding: 0px 30px 0px 94px; - position: absolute; - right: 28px; - top: 11px; -} - -.question-border::before { - background: #2e3c45; - bottom: 6px; - box-shadow: 3px 3px 3px rgba(24, 24, 24, 0.5); - content: ''; - left: 6px; - position: absolute; - right: 6px; - top: 6px; -} - -.question-icon { - bottom: 0px; - height: 60px; - left: 2px; - position: absolute; - top: 2px; - transition: opacity 0.2s ease; - width: 60px; -} - -.question-message { - color: #2b3940; - font-size: 23px; - width: 100%; - z-index: 0; -} - -.question-message span { - display: block; - letter-spacing: normal; -} - -.question-outer { - height: 86px; - position: relative; -} - -.question::before { - width: 70px; - background: #000000; - bottom: 0px; - content: ''; - left: 0px; - position: absolute; - top: 0px; -} - -.ret-icon { - display: inline-block; - height: 58px; - transition: opacity 0.2s ease; - width: 58px; -} - -.ret-icon-wrapper { - margin-left: -4px; - position: relative; - will-change: transform; + flex-direction: column; } +/* Checkbox element (hidden) */ #saveDefaults { - /* Checkbox element (hidden) */ - left: -100vw; position: absolute; + left: -100vw; } +.checkbox-display { + margin: 10px 70px; +} + +/* Displayed Checkbox (unchecked) */ +.checkbox-display::after { + content: "\E14C"; + color: white; +} + +/* Displayed Checkbox (checked) */ #saveDefaults:checked~.checkbox-display::after { - /* Displayed Checkbox (checked) */ content: "\E14B"; } -a { - text-decoration: none; +.menu-item { + /* Container div for menu link and menu list */ + width: 30%; + margin: 6px; } -body { - background: none; - width: 1280px; -} - -body, div, th, td, p, ul, ol, dl, dt, dd, img, h1, h2, h3, h4, h5, h6, footer, header, nav, p, section, span, figure { - margin: 0px; - overflow-wrap: break-word; +.menu-button, .menu-item > div button { + /* Item styling */ + background-color: #d9e4ea; + border: solid black; + border-width: 3px 20px 3px 3px; padding: 0px; - word-break: normal; - font-family: "nintendo_ext_003", "nintendo_udsgr_std_003"; + box-shadow: 3px 3px 3px #18181880; + display: flex; + height: 60px; + width: 100%; + align-items: center; + align-content: center; + user-select: none; } -img, svg { - opacity: 0; +.menu-item > div button { + z-index: 1; + width: 22%; + margin: 4px 17px; } -img.question-icon:not(.toggle) { - /* Fade icons slightly */ - opacity: 0.75; +.modal p { + font-size: 18px !important; } -span { - letter-spacing: 0.01px; +.menu-icon { + background-color: black; + flex-basis: auto; + width: 60px; + height: 48px; + border: solid black; + border-right-width: 5px; +} + +.menu-icon img { + width: 100%; + height: 100%; +} + +.menu-item p { + font-size: 23px; + color: #2b3940; + width: 100%; + margin: 10px 0px 0px 20px; +} + +.hide { + display: none !important; +} + +.menu-item > div { + display: flex; + flex-wrap: wrap; + position: fixed; + justify-content: flex-start; + align-content: flex-start; + top: 80px; + bottom: 73px; + left: 0px; + right: 0px; + margin-top: 0px; + background-color: rgba(100, 100, 100, 0.9); +} + +:focus { + background: rgb(255,70,2); + background: linear-gradient(45deg, rgba(255,70,2,1) 20%, rgba(255,215,0,1) 46%, rgba(255,215,0,1) 54%, rgba(255,70,2,1) 80%); + background-size: 500% 100%; + will-change: animation; + animation: translate-anim 5s infinite linear; +} + +:focus > p { + color: #fff; + text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000; +} + +@keyframes translate-anim { + 0% { + background-position: 0% 0%; + } + 100% { + background-position: 100% 0%; + } } -ul, ol { - list-style-type: none; -} \ No newline at end of file diff --git a/src/static/img/quick_menu.svg b/src/static/img/quick_menu.svg new file mode 100644 index 0000000..14cfdc8 --- /dev/null +++ b/src/static/img/quick_menu.svg @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="80" + height="80" + viewBox="0 0 21.166666 21.166667" + version="1.1" + id="svg5" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs2" /> + <g + id="layer1"> + <path + style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 1.8552457,17.110952 8.469829,10.496369 1.8552457,3.8817854" + id="path1904" /> + <path + style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 7.22203,17.110952 13.836612,10.49637 7.22203,3.8817855" + id="path1904-3" /> + <path + style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 12.588812,17.110952 19.203396,10.496369 12.588812,3.8817854" + id="path1904-2" /> + </g> +</svg> diff --git a/src/static/js/training_modpack.js b/src/static/js/training_modpack.js index 8cc210f..8776bff 100644 --- a/src/static/js/training_modpack.js +++ b/src/static/js/training_modpack.js @@ -1,22 +1,23 @@ var isNx = (typeof window.nx !== 'undefined'); -var prevQuestionMsg = null; -var prevFocusedElm = null; +var defaults_prefix = "__"; if (isNx) { - window.nx.footer.setAssign('B', '', goBackHook, {se: ''}); - window.nx.footer.setAssign('X', '', resetSubmenu, {se: ''}); + window.nx.footer.setAssign('B', '', close_or_exit, {se: ''}); + window.nx.footer.setAssign('X', '', resetCurrentSubmenu, {se: ''}); window.nx.footer.setAssign('L', '', resetAllSubmenus, {se: ''}); - window.nx.footer.setAssign('R', '', toggleSaveDefaults, {se: ''}); + window.nx.footer.setAssign('R', '', saveDefaults, {se: ''}); + window.nx.footer.setAssign('ZR', '', cycleNextTab, {se: ''}); + window.nx.footer.setAssign('ZL', '', cyclePrevTab, {se: ''}); } else { - document.getElementById("body").addEventListener('keypress', (event) => { + document.addEventListener('keypress', (event) => { switch (event.key) { case "b": console.log("b"); - goBackHook(); + close_or_exit(); break; case "x": console.log("x"); - resetSubmenu(); + resetCurrentSubmenu(); break; case "l": console.log("l"); @@ -24,20 +25,87 @@ if (isNx) { break; case "r": console.log("r"); - toggleSaveDefaults(); + saveDefaults(); + break; + case "p": + console.log("p"); + cycleNextTab(); + break; + case "o": + console.log("o"); + cyclePrevTab(); break; } }); } -window.onload = setSettings; +window.onload = onLoad; +var settings = new Map(); +var lastFocusedItem = document.querySelector(".menu-item > button"); -function isTextNode(node) { - return node.nodeType == Node.TEXT_NODE +function onLoad() { + // Activate the first tab + openTab(document.querySelector("button.tab-button")); + + // Extract URL params and set appropriate settings + setSettingsFromURL(); + setSubmenusFromSettings(); +} + +function openTab(e) { + var tab_id = e.id.replace("button", "tab"); + var selected_tab = document.getElementById(tab_id); + + + // Hide all content for all tabs + closeAllItems(); + tabcontent = document.getElementsByClassName("tab-content"); + Array.from(tabcontent).forEach(element => { + element.classList.add("hide"); + }); + + + // Get all elements with class="tablinks" and remove the class "active" + tablinks = document.getElementsByClassName("tab-button"); + Array.from(tablinks).forEach(element => { + element.classList.remove("active"); + }); + + // Show the current tab, and add an "active" class to the button that opened the tab + e.classList.add("active"); + selected_tab.classList.remove("hide"); + selected_tab.querySelector("button").focus(); +} + +function openItem(e) { + playSound("SeWebMenuListOpen"); + var modal = e.parentElement.querySelector(".modal"); + modal.classList.toggle("hide"); + modal.querySelector("button").focus(); + lastFocusedItem = e; +} + +function closeAllItems() { + var modals = document.querySelectorAll(".modal"); + Array.from(modals).forEach(element => { + element.classList.add("hide"); + }); + lastFocusedItem.focus(); +} + +function toggleOption(e) { + playSound("SeSelectCheck"); + if (e.parentElement.classList.contains("single-option")) { + selectSingleOption(e); + } else { + var img = e.querySelector("img"); + img.classList.toggle("hide"); + } } function closestClass(elem, class_) { - // Returns the closest anscestor (including self) with the given class + // Returns the closest ancestor (including self) with the given class + // TODO: Consider removing if (!elem) { // Reached the end of the DOM return null @@ -49,46 +117,6 @@ function closestClass(elem, class_) { return closestClass(elem.parentElement, class_); } } - -function getElementByXpath(path) { - return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; -} - -function focusQA(e) { - playSound("SeSelectUncheck"); - prevFocusedElm = e; - e.classList.add("is-focused"); -} - -function defocusQA(e) { - if (prevFocusedElm) { - prevFocusedElm.classList.remove('is-focused'); - - } - if (prevQuestionMsg) { - prevQuestionMsg.remove(); - prevQuestionMsg = null; - } -} - -function toggleAnswer(e) { - playSound("SeToggleBtnOn"); - e.classList.toggle("is-opened"); - - // Toggle visibility of child answers - [].forEach.call(e.childNodes, function (child) { - if (!isTextNode(child) && child.classList.contains("answer-border-outer")) { - child.classList.toggle("is-hidden"); - } - }); - - // Toggle visibility of sibling answers - var sibling = e.nextElementSibling; - if (sibling.classList.contains("answer-border-outer")) { - sibling.classList.toggle("is-hidden"); - } -} - function playSound(label) { // Valid labels: // SeToggleBtnFocus @@ -129,164 +157,120 @@ function playSound(label) { } } -function goBackHook() { +function exit() { + playSound("SeFooterDecideBack"); + setSettingsFromMenu(); + var url = buildURLFromSettings(); + + if (isNx) { + window.location.href = url; + } else { + console.log(url); + } +} + +function close_or_exit() { // If any submenus are open, close them // Otherwise if all submenus are closed, exit the menu and return to the game - if (document.querySelectorAll(".qa.is-opened").length == 0) { - // If all submenus are closed, exit and return through localhost - playSound("SeFooterDecideBack"); - var url = "http://localhost/"; - - var settings = new Map(); - - // Collect settings for toggles - - [].forEach.call(document.querySelectorAll("ul.l-grid"), function (toggle) { - var section = toggle.id; - var val = ""; - - [].forEach.call(toggle.childNodes, function (child) { - if (!isTextNode(child) && child.querySelectorAll(".is-appear").length) { - val += child.getAttribute("val") + ","; - }; - }); - - settings.set(section,val); - }); - - // Collect settings for OnOffs - [].forEach.call(document.querySelectorAll("div.onoff"), function (onoff) { - var section = onoff.id; - var val = ""; - if (onoff.querySelectorAll(".is-appear").length) { - val = "1"; - } else { - val = "0"; - } - settings.set(section,val); - }); - - url += "?"; - settings.forEach((val, section) => { url += section + "=" + val + "&" } ); - - if (document.getElementById("saveDefaults").checked) { - url += "save_defaults=1"; - } else { - url = url.slice(0, -1); - } - - if (isNx) { - window.location.href = url; - } else { - console.log(url); - } - } else { + if (document.querySelector(".modal:not(.hide)")) { // Close any open submenus - [].forEach.call(document.querySelectorAll(".qa.is-opened"), function (submenu) { toggleAnswer(submenu); }); + console.log("Closing Items"); + closeAllItems(); + } else { + // If all submenus are closed, exit and return through localhost + console.log("Exiting"); + exit(); } } -function clickToggle(e) { - playSound("SeCheckboxOn"); - var toggleOptions = e.querySelector(".toggle"); - if (e.querySelector(".is-single-option")) { // Single-option submenu - // Deselect all submenu options - closestClass(e, "l-qa").querySelector(".toggle").classList.remove("is-appear"); - closestClass(e, "l-qa").querySelector(".toggle").classList.add("is-hidden"); - // Then set the current one as the active setting - toggleOptions.classList.add("is-appear"); - toggleOptions.classList.remove("is-hidden"); - } else { // Multi-option submenu - toggleOptions.classList.toggle("is-appear"); - toggleOptions.classList.toggle("is-hidden"); - } -} - -function getParams(url) { +function setSettingsFromURL() { var regex = /[?&]([^=#]+)=([^&#]*)/g, - params = {}, match; - while (match = regex.exec(url)) { - params[match[1]] = match[2]; + while (match = regex.exec(document.URL)) { + settings.set(match[1], match[2]); } - return params; } -function setSettings() { - // Get settings from the URL GET parameters - const settings = getParams(document.URL); - - // Set Toggles - [].forEach.call(document.querySelectorAll("ul.l-grid"), function (toggle) { - var section = toggle.id; - var section_setting = decodeURIComponent(settings[section]); - - [].forEach.call(toggle.querySelectorAll("li"), function (child) { - var e = child.querySelector("img.toggle"); - if (section_setting.split(",").includes(child.getAttribute("val"))) { - e.classList.add("is-appear"); - e.classList.remove("is-hidden"); - } else { - e.classList.remove("is-appear"); - e.classList.add("is-hidden"); - }; - }); +function setSettingsFromMenu() { + var section; + var mask; + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + section = menuItem.id; + mask = getMaskFromSubmenu(menuItem); + settings.set(section, mask); }); +} - // Set OnOffs - [].forEach.call(document.querySelectorAll("div.onoff"), function (onOff) { - var section = onOff.id; - var section_setting = decodeURIComponent(settings[section]); - var e = onOff.querySelector("img.toggle"); - if (section_setting == "1") { - e.classList.add("is-appear"); - e.classList.remove("is-hidden"); +function buildURLFromSettings() { + var url = "http://localhost/"; + url += "?"; + settings.forEach((val, key) => { url += key + "=" + String(val) + "&" } ); + return url +} + +function selectSingleOption(e) { + // Deselect all options in the submenu + parent = closestClass(e, "single-option"); + siblings = parent.querySelectorAll(".menu-icon img"); + [].forEach.call(siblings, function (sibling) { + sibling.classList.add("hide"); + }); + e.querySelector(".menu-icon img").classList.remove("hide"); +} + +function setSubmenusFromSettings() { + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + var section = menuItem.id; + var section_mask = decodeURIComponent(settings.get(section)); + setSubmenuByMask(menuItem, section_mask) + }); +} + +function setSubmenuByMask(menuItem, mask) { + [].forEach.call(menuItem.querySelectorAll(".modal .menu-icon img"), function (toggle) { + if (isInBitmask(toggle.dataset.val, mask)) { + toggle.classList.remove("hide"); } else { - e.classList.remove("is-appear"); - e.classList.add("is-hidden"); - }; - }); -} - -function resetSubmenu() { - // Resets any open or focused submenus to the default values - playSound("SeToggleBtnOff"); - [].forEach.call(document.querySelectorAll("[default*='is-appear']"), function (item) { - if (isSubmenuFocused(item)) { - item.classList.add("is-appear"); - item.classList.remove("is-hidden"); + toggle.classList.add("hide"); } }); - [].forEach.call(document.querySelectorAll("[default*='is-hidden']"), function (item) { - if (isSubmenuFocused(item)) { - item.classList.remove("is-appear"); - item.classList.add("is-hidden"); - } - }); + // If no setting for a Single Option is set, select the first one + var isSingleOption = menuItem.querySelectorAll(".modal.single-option").length != 0; + var isAllDeselected = menuItem.querySelectorAll(".modal .menu-icon img:not(.hide)").length == 0; + if (isSingleOption & isAllDeselected) { + selectSingleOption(menuItem.querySelector(".modal button")); + } } -function isSubmenuFocused(elem) { - // Return true if the element is in a submenu which is either focused or opened - return ( - closestClass(elem, "l-qa").querySelectorAll(".is-opened, .is-focused").length - || closestClass(elem, "is-focused") - ) +function getMaskFromSubmenu(menuItem) { + var val = 0; + [].forEach.call(menuItem.querySelectorAll(".modal img:not(.hide)"), function (toggle) { + val += parseInt(toggle.dataset.val); + }); + return val +} + +function resetCurrentSubmenu() { + var focus = document.querySelector(".menu-item .modal:not(.hide)"); + if (!focus) { + focus = document.querySelector(":focus"); + } + var menuItem = closestClass(focus, "menu-item"); + + var key = defaults_prefix + menuItem.id; + var section_mask = decodeURIComponent(settings.get(key)); + setSubmenuByMask(menuItem, section_mask); } function resetAllSubmenus() { // Resets all submenus to the default values if (confirm("Are you sure that you want to reset all menu settings to the default?")) { - playSound("SeToggleBtnOff"); - [].forEach.call(document.querySelectorAll("[default*='is-appear']"), function (item) { - item.classList.add("is-appear"); - item.classList.remove("is-hidden"); - }); - - [].forEach.call(document.querySelectorAll("[default*='is-hidden']"), function (item) { - item.classList.remove("is-appear"); - item.classList.add("is-hidden"); + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + var key = defaults_prefix + menuItem.id; + var mask = decodeURIComponent(settings.get(key)); + setSubmenuByMask(menuItem, mask) }); } } @@ -296,9 +280,42 @@ function setHelpText(text) { document.getElementById("help-text").innerText = text; } -function toggleSaveDefaults() { - // Change the status of the Save Defaults checkbox - playSound("SeCheckboxOn"); - var saveDefaultsCheckbox = document.getElementById("saveDefaults"); - saveDefaultsCheckbox.checked = !saveDefaultsCheckbox.checked; +function saveDefaults() { + if (confirm("Are you sure that you want to change the default menu settings to the current selections?")) { + var key; + var mask; + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + key = defaults_prefix + menuItem.id; + mask = getMaskFromSubmenu(menuItem); + settings.set(key, mask); + }); + } +} + +function isInBitmask(val, mask) { + // Return true if the value is in the bitmask + return (mask & val) != 0 +} + +function cycleNextTab() { + // Cycle to the next tab + var activeTab = document.querySelector(".tab-button.active"); + var nextTab = activeTab.nextElementSibling; + if (!nextTab) { + // On the last tab - set the next tab as the first tab in the list + nextTab = document.querySelector(".tab-button"); + } + openTab(nextTab); +} + +function cyclePrevTab() { + // Cycle to the previous tab + var activeTab = document.querySelector(".tab-button.active"); + var prevTab = activeTab.previousElementSibling; + if (!prevTab) { + // On the first tab - set the next tab as the last tab in the list + tabs = document.querySelectorAll(".tab-button"); + prevTab = tabs[tabs.length - 1]; + } + openTab(prevTab); } \ No newline at end of file diff --git a/src/templates/menu.html b/src/templates/menu.html index 37b52e5..4041aa8 100644 --- a/src/templates/menu.html +++ b/src/templates/menu.html @@ -1,117 +1,59 @@ <!DOCTYPE html> <html lang="en"> - <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> - <title>Document</title> - <link rel="stylesheet" href="./css/training_modpack.css" /> - </head> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> + <title>Modpack Menu</title> + <link rel="stylesheet" href="./css/training_modpack.css" /> +</head> - <body id="body"> - <script defer src="./js/training_modpack.js"></script> - <div class="l-header"> - <div class="l-header-title"> - <div class="header-title"><span>Ultimate Training Modpack Menu</span></div> - </div> - <div class="header" style="flex-grow: 1;"> - <a id="ret-button" tabindex="-1" class="header-decoration" href="javascript:goBackHook();"> - <div class="ret-icon-wrapper"> - <img class="ret-icon is-appear" src="./img/m_retnormal.svg"> - </div> - </a> - </div> - <div class="header" style="flex-direction: column; justify-content: center; align-items: end; padding-right: 10px;"> - <p class="header-desc">Reset Current Menu: </p> - <p class="header-desc">Reset All Menus: </p> - </div> +<body> + <script defer src="./js/training_modpack.js"></script> + <div class="l-header"> + <a id="ret-button" tabindex="-1" class="return-icon-container" href="javascript:goBackHook();"> + <img class="return-icon" src="./img/m_retnormal.svg"> + </a> + <p class="header-title">Ultimate Training Modpack Menu</p> + <div class="header-label"> + <p class="header-desc">Reset Current Menu: </p> + <p class="header-desc">Reset All Menus: </p> + <p class="header-desc">Save Defaults: </p> </div> - <br> - <br> - <br> - <br> - - <div class="l-grid"> - - <!-- - Script the part below via templating. Overall structure is perhaps - [ - l-qa qa [id=qa-{{menuName}} tabindex="{{index}}"] { - // make question for {{menuName}} - // make answer with l-grid : l-item list for options - }, - ... - ] - - - Remember to set make max keyword size greater than 23! - --> - {{#sub_menus}} - <div class="l-qa"> - {{^onoffselector}} - <a id="qa-{{id}}" class="qa" tabindex="{{index}}" href="javascript:void(0);" onfocus="focusQA(this);setHelpText({{help_text}})" onblur="defocusQA(this)" onclick="toggleAnswer(this)"> - <div class="question-outer"> - <div class="question-border"> - <div id="question-{{id}}" class="question scuffle-thema"> - <img class="question-icon" src="./img/{{id}}.svg" /> - <p class="question-message"> - <span>{{title}}</span> - </p> - </div> - </div> - </div> - </a> - <div id="answer-border-{{id}}" class="answer-border-outer is-hidden"> - <div class="l-main"> - <ul class="l-grid" id="{{id}}"> - {{#toggles}} - <li class="l-item" val="{{value}}"> - <div class="keyword-button-outer"> - <a tabindex="{{index}}" class="flex-button keyword-button scuffle-thema" href="javascript:void(0)" onclick="clickToggle(this);"> - <div class="button-icon-wrapper"> - <img class="button-icon toggle {{checked}} {{#is_single_option}}is-single-option{{/is_single_option}}" src="./img/check.svg" default="{{default}}"> - </div> - <div class="button-msg-wrapper"> - <div class="keyword-message"> - {{title}} - </div> - </div> - </a> - </div> - </li> - {{/toggles}} - </ul> - </div> - </div> - {{/onoffselector}} - {{#onoffselector}} - <a id="qa-{{id}}" class="qa" tabindex="{{index}}" href="javascript:void(0);" onfocus="focusQA(this);setHelpText({{help_text}})" onblur="defocusQA(this)" onclick="clickToggle(this)"> - <div class="question-outer"> - <div class="question-border"> - <div id="question-{{id}}" class="question scuffle-thema"> - <div id="{{id}}" class="onoff"> - <img class="question-icon" style="z-index: 1;" src="./img/{{id}}.svg" /> - <div><img class="question-icon toggle {{checked}}" style="z-index: 2;" src="./img/check.svg" default="{{default}}"/></div> - <p class="question-message"> - <span>{{title}}</span> - </p> - </div> - </div> - </div> - </div> - </a> - {{/onoffselector}} + </div> + <div class="tab-list-container"> + <p>Prev Tab: </p> + <div class="tab-list"> + {{#tabs}} + <button class="tab-button active" id="{{tab_id}}_button" onclick="openTab(this)" tabindex="-1">{{tab_title}}</button> + {{/tabs}} + </div> + <p>Next Tab: </p> + </div> + <div class="tab-content-container"> + {{#tabs}} + <div id="{{tab_id}}_tab" class="tab-content"> + {{#tab_submenus}} + <div class="menu-item" id="{{submenu_id}}"> + <button class="menu-button" onfocus="setHelpText('{{help_text}}')" onclick="openItem(this)"> + <div class="menu-icon"><img src="./img/{{submenu_id}}.svg" /></div> + <p>{{submenu_title}}</p> + </button> + <div class="hide modal{{#is_single_option}} single-option{{/is_single_option}}"> + {{#toggles}} + <button class="menu-button" onclick="toggleOption(this)"> + <div class="menu-icon"><img class="hide" src="./img/check.svg" data-val="{{toggle_value}}"/></div> + <p>{{toggle_title}}</p> + </button> + {{/toggles}} </div> - {{/sub_menus}} - </div> - <footer id="footer" class="footer l-footer"> - <p id="help-text" class="header-desc"></p> - <div class="defaults-checkbox-container"> - <label class="header-desc" for="saveDefaults">Save defaults: </label> - <input type="checkbox" id="saveDefaults"> - <div class="checkbox-display"></div> </div> - </footer> - </body> - + {{/tab_submenus}} + </div> + {{/tabs}} + </div> + <footer id="footer" class="footer"> + <p id="help-text" class="header-desc"></p> + </footer> +</body> </html> diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index 7972bb6..1f0798b 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -5,13 +5,18 @@ extern crate bitflags; extern crate num_derive; use core::f64::consts::PI; +use std::collections::HashMap; #[cfg(feature = "smash")] use smash::lib::lua_const::*; +use strum::IntoEnumIterator; use strum_macros::EnumIter; use serde::{Serialize, Deserialize}; use ramhorns::Content; -use strum::IntoEnumIterator; -use std::ops::BitOr; + +pub trait ToggleTrait { + fn to_toggle_strs() -> Vec<&'static str>; + fn to_toggle_vals() -> Vec<usize>; +} // bitflag helper function macro macro_rules! extra_bitflag_impls { @@ -56,22 +61,21 @@ macro_rules! extra_bitflag_impls { } } } - - pub fn to_toggle_strs() -> Vec<&'static str> { + } + impl ToggleTrait for $e { + fn to_toggle_strs() -> Vec<&'static str> { let all_options = <$e>::all().to_vec(); all_options.iter().map(|i| i.as_str().unwrap_or("")).collect() } - pub fn to_toggle_vals() -> Vec<usize> { + fn to_toggle_vals() -> Vec<usize> { let all_options = <$e>::all().to_vec(); all_options.iter().map(|i| i.bits() as usize).collect() } - pub fn to_url_param(&self) -> String { - self.to_vec() - .into_iter() - .map(|field| field.bits().to_string()) - .collect::<Vec<_>>() - .join(",") + } + impl ToUrlParam for $e { + fn to_url_param(&self) -> String { + self.bits().to_string() } } } @@ -258,10 +262,10 @@ extra_bitflag_impls! {MissTechFlags} #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize, Deserialize)] pub enum Shield { - None = 0, - Infinite = 1, - Hold = 2, - Constant = 3, + None = 0x0, + Infinite = 0x1, + Hold = 0x2, + Constant = 0x4, } impl Shield { @@ -279,13 +283,23 @@ impl Shield { } } +impl ToggleTrait for Shield { + fn to_toggle_strs() -> Vec<&'static str> { + Shield::iter().map(|i| i.as_str().unwrap_or("")).collect() + } + + fn to_toggle_vals() -> Vec<usize> { + Shield::iter().map(|i| i as usize).collect() + } +} + // Save State Mirroring #[repr(i32)] #[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, EnumIter, Serialize, Deserialize)] pub enum SaveStateMirroring { - None = 0, - Alternate = 1, - Random = 2, + None = 0x0, + Alternate = 0x1, + Random = 0x2, } impl SaveStateMirroring { @@ -302,6 +316,16 @@ impl SaveStateMirroring { } } +impl ToggleTrait for SaveStateMirroring { + fn to_toggle_strs() -> Vec<&'static str> { + SaveStateMirroring::iter().map(|i| i.as_str().unwrap_or("")).collect() + } + + fn to_toggle_vals() -> Vec<usize> { + SaveStateMirroring::iter().map(|i| i as usize).collect() + } +} + // Defensive States bitflags! { #[derive(Serialize, Deserialize)] @@ -357,6 +381,15 @@ impl OnOff { } } +impl ToggleTrait for OnOff { + fn to_toggle_strs() -> Vec<&'static str> { + vec!["Off", "On"] + } + fn to_toggle_vals() -> Vec<usize> { + vec![0, 1] + } +} + bitflags! { #[derive(Serialize, Deserialize)] pub struct Action : u32 { @@ -861,6 +894,16 @@ impl SdiStrength { } } +impl ToggleTrait for SdiStrength { + fn to_toggle_strs() -> Vec<&'static str> { + SdiStrength::iter().map(|i| i.as_str().unwrap_or("")).collect() + } + + fn to_toggle_vals() -> Vec<usize> { + SdiStrength::iter().map(|i| i as usize).collect() + } +} + // For input delay trait ToUrlParam { fn to_url_param(&self) -> String; @@ -887,9 +930,14 @@ macro_rules! url_params { $(pub $field_name: $field_type,)* } impl $e { - pub fn to_url_params(&self) -> String { - let mut s = "?".to_string(); + pub fn to_url_params(&self, defaults: bool) -> String { + let mut s = "".to_string(); + let defaults_str = match defaults { + true => &"__", // Prefix field name with "__" if it is for the default menu + false => &"", + }; $( + s.push_str(defaults_str); s.push_str(stringify!($field_name)); s.push_str(&"="); s.push_str(&self.$field_name.to_url_param()); @@ -1004,35 +1052,10 @@ pub enum FighterId { CPU = 1, } -#[derive(Content, Clone)] -pub struct Slider { - pub min: usize, - pub max: usize, - pub index: usize, - pub value: usize, -} - -#[derive(Content, Clone)] -pub struct Toggle<'a> { - pub title: &'a str, - pub checked: &'a str, - pub index: usize, - pub value: usize, - pub default: &'a str, -} - -#[derive(Content, Clone)] -pub struct OnOffSelector<'a> { - pub title: &'a str, - pub checked: &'a str, - pub default: &'a str, -} - #[derive(Clone)] pub enum SubMenuType { TOGGLE, SLIDER, - ONOFF, } impl SubMenuType { @@ -1040,67 +1063,12 @@ impl SubMenuType { match s { "toggle" => SubMenuType::TOGGLE, "slider" => SubMenuType::SLIDER, - "onoff" => SubMenuType::ONOFF, _ => panic!("Unexpected SubMenuType!") } } } -#[derive(Content, Clone)] -pub struct SubMenu<'a> { - pub title: &'a str, - pub id: &'a str, - pub _type: &'a str, - pub toggles: Vec<Toggle<'a>>, - pub sliders: Vec<Slider>, - pub onoffselector: Vec<OnOffSelector<'a>>, - pub index: usize, - pub check_against: usize, - pub is_single_option: Option<bool>, - pub help_text: &'a str, -} - -impl<'a> SubMenu<'a> { - pub fn max_idx(&self) -> usize { - self.toggles - .iter() - .max_by(|t1, t2| t1.index.cmp(&t2.index)) - .map(|t| t.index) - .unwrap_or(self.index) - } - - pub fn add_toggle(&mut self, title: &'a str, checked: bool, value: usize, default: bool) { - self.toggles.push(Toggle { - title, - checked: if checked { "is-appear" } else { "is-hidden" }, - index: self.max_idx() + 1, - value, - default: if default { "is-appear" } else { "is-hidden" }, - }); - } - - pub fn add_slider(&mut self, min: usize, max: usize, value: usize) { - self.sliders.push(Slider { - min, - max, - index: self.max_idx() + 1, - value, - }); - } - - pub fn add_onoffselector(&mut self, title: &'a str, checked: bool, default: bool) { - // TODO: Is there a more elegant way to do this? - // The HTML only supports a single onoffselector but the SubMenu stores it as a Vec - self.onoffselector.push(OnOffSelector { - title, - checked: if checked { "is-appear" } else { "is-hidden" }, - default: if default { "is-appear" } else { "is-hidden" }, - }); - } -} - - -pub static DEFAULT_MENU: TrainingModpackMenu = TrainingModpackMenu { +pub static DEFAULTS_MENU: TrainingModpackMenu = TrainingModpackMenu { hitbox_vis: OnOff::On, stage_hazards: OnOff::Off, di_state: Direction::empty(), @@ -1137,452 +1105,106 @@ pub static DEFAULT_MENU: TrainingModpackMenu = TrainingModpackMenu { quick_menu: OnOff::Off, }; -pub static mut MENU: TrainingModpackMenu = DEFAULT_MENU; +pub static mut MENU: TrainingModpackMenu = DEFAULTS_MENU; #[derive(Content, Clone)] -pub struct Menu<'a> { - pub sub_menus: Vec<SubMenu<'a>>, +pub struct Slider { + pub min: usize, + pub max: usize, + pub index: usize, + pub value: usize, } -impl<'a> Menu<'a> { - pub fn max_idx(&self) -> usize { - self.sub_menus - .iter() - .max_by(|x, y| x.max_idx().cmp(&y.max_idx())) - .map(|sub_menu| sub_menu.max_idx()) - .unwrap_or(0) - } - - pub fn add_sub_menu( - &mut self, - title: &'a str, - id: &'a str, - _type: &'a str, - check_against: usize, - toggles: Vec<(&'a str, usize)>, - sliders: Vec<(usize, usize, usize)>, - defaults: usize, - help_text: &'a str, - ) { - let mut sub_menu = SubMenu { - title, - id, - _type, - toggles: Vec::new(), - sliders: Vec::new(), - onoffselector: Vec::new(), - index: self.max_idx() + 1, - check_against, - is_single_option: Some(true), - help_text, - }; - - for toggle in toggles { - sub_menu.add_toggle( - toggle.0, - (check_against & toggle.1) != 0, - toggle.1, - (defaults & toggle.1) != 0, - ) - } - - for slider in sliders { - sub_menu.add_slider(slider.0, slider.1, slider.2); - } - - self.sub_menus.push(sub_menu); - } - - pub fn add_sub_menu_sep( - &mut self, - title: &'a str, - id: &'a str, - _type: &'a str, - check_against: usize, - strs: Vec<&'a str>, - vals: Vec<usize>, - defaults: usize, - help_text: &'a str, - ) { - let mut sub_menu = SubMenu { - title, - id, - _type, - toggles: Vec::new(), - sliders: Vec::new(), - onoffselector: Vec::new(), - index: self.max_idx() + 1, - check_against, - is_single_option: None, - help_text, - }; - - for i in 0..strs.len() { - sub_menu.add_toggle( - strs[i], - (check_against & vals[i]) != 0, - vals[i], - (defaults & vals[i]) != 0, - ) - } - - // TODO: add sliders? - - self.sub_menus.push(sub_menu); - } - - pub fn add_sub_menu_onoff( - &mut self, - title: &'a str, - id: &'a str, - _type: &'a str, - check_against: usize, - checked: bool, - default: usize, - help_text: &'a str, - ) { - let mut sub_menu = SubMenu { - title, - id, - _type, - toggles: Vec::new(), - sliders: Vec::new(), - onoffselector: Vec::new(), - index: self.max_idx() + 1, - check_against, - is_single_option: None, - help_text, - }; - - sub_menu.add_onoffselector(title, checked, (default & OnOff::On as usize) != 0); - self.sub_menus.push(sub_menu); - } +#[derive(Content, Clone)] +pub struct Toggle<'a> { + pub toggle_value: usize, + pub toggle_title: &'a str, + pub checked: bool, } -macro_rules! add_bitflag_submenu { - ($menu:ident, $title:literal, $id:ident, $e:ty, $help_text:literal) => { - paste::paste!{ - let [<$id _strs>] = <$e>::to_toggle_strs(); - let [<$id _vals>] = <$e>::to_toggle_vals(); - - $menu.add_sub_menu_sep( - $title, - stringify!($id), - "toggle", - MENU.$id.bits() as usize, - [<$id _strs>], - [<$id _vals>], - DEFAULT_MENU.$id.bits() as usize, - stringify!($help_text), - ); - } - } +#[derive(Content, Clone)] +pub struct SubMenu<'a> { + pub submenu_title: &'a str, + pub submenu_id: &'a str, + pub help_text: &'a str, + pub is_single_option: bool, + pub toggles: Vec<Toggle<'a>>, + pub _type: &'a str, } -macro_rules! add_single_option_submenu { - ($menu:ident, $title:literal, $id:ident, $e:ty, $help_text:literal) => { - paste::paste!{ - let mut [<$id _toggles>] = Vec::new(); - for val in [<$e>]::iter() { - [<$id _toggles>].push((val.as_str().unwrap_or(""), val as usize)); +impl<'a> SubMenu<'a> { + pub fn add_toggle( + &mut self, + toggle_value: usize, + toggle_title: &'a str + ) { + self.toggles.push( + Toggle { + toggle_value: toggle_value, + toggle_title: toggle_title, + checked: false } - - $menu.add_sub_menu( - $title, - stringify!($id), - "toggle", - MENU.$id as usize, - [<$id _toggles>], - [].to_vec(), - DEFAULT_MENU.$id as usize, - stringify!($help_text), - ); - } + ); + } + pub fn new_with_toggles<T:ToggleTrait>( + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + is_single_option: bool, + ) -> SubMenu<'a> { + let mut instance = SubMenu { + submenu_title: submenu_title, + submenu_id: submenu_id, + help_text: help_text, + is_single_option: is_single_option, + toggles: Vec::new(), + _type: "toggle" + }; + + let values = T::to_toggle_vals(); + let titles = T::to_toggle_strs(); + for i in 0..values.len() { + instance.add_toggle( + values[i], + titles[i], + ); + } + instance } } -macro_rules! add_onoff_submenu { - ($menu:ident, $title:literal, $id:ident, $help_text:literal) => { - paste::paste! { - $menu.add_sub_menu_onoff( - $title, - stringify!($id), - "onoff", - MENU.$id as usize, - (MENU.$id as usize & OnOff::On as usize) != 0, - DEFAULT_MENU.$id as usize, - stringify!($help_text), - ); - } - }; +#[derive(Content)] +pub struct Tab<'a> { + pub tab_id: &'a str, + pub tab_title: &'a str, + pub tab_submenus: Vec<SubMenu<'a>>, } -pub unsafe fn get_menu() -> Menu<'static> { - let mut overall_menu = Menu { - sub_menus: Vec::new(), - }; - - // Toggle/bitflag menus - add_bitflag_submenu!( - overall_menu, - "Mash Toggles", - mash_state, - Action, - "Mash Toggles: Actions to be performed as soon as possible" - ); - add_bitflag_submenu!( - overall_menu, - "Followup Toggles", - follow_up, - Action, - "Followup Toggles: Actions to be performed after the Mash option" - ); - add_bitflag_submenu!( - overall_menu, - "Attack Angle", - attack_angle, - AttackAngle, - "Attack Angle: For attacks that can be angled, such as some forward tilts" - ); - - add_bitflag_submenu!( - overall_menu, - "Ledge Options", - ledge_state, - LedgeOption, - "Ledge Options: Actions to be taken when on the ledge" - ); - add_bitflag_submenu!( - overall_menu, - "Ledge Delay", - ledge_delay, - LongDelay, - "Ledge Delay: How many frames to delay the ledge option" - ); - add_bitflag_submenu!( - overall_menu, - "Tech Options", - tech_state, - TechFlags, - "Tech Options: Actions to take when slammed into a hard surface" - ); - add_bitflag_submenu!( - overall_menu, - "Miss Tech Options", - miss_tech_state, - MissTechFlags, - "Miss Tech Options: Actions to take after missing a tech" - ); - add_bitflag_submenu!( - overall_menu, - "Defensive Options", - defensive_state, - Defensive, - "Defensive Options: Actions to take after a ledge option, tech option, or miss tech option" - ); - - add_bitflag_submenu!( - overall_menu, - "Aerial Delay", - aerial_delay, - Delay, - "Aerial Delay: How long to delay a Mash aerial attack" - ); - add_bitflag_submenu!( - overall_menu, - "OoS Offset", - oos_offset, - Delay, - "OoS Offset: How many times the CPU shield can be hit before performing a Mash option" - ); - add_bitflag_submenu!( - overall_menu, - "Reaction Time", - reaction_time, - Delay, - "Reaction Time: How many frames to delay before performing an option out of shield" - ); - - add_bitflag_submenu!( - overall_menu, - "Fast Fall", - fast_fall, - BoolFlag, - "Fast Fall: Should the CPU fastfall during a jump" - ); - add_bitflag_submenu!( - overall_menu, - "Fast Fall Delay", - fast_fall_delay, - Delay, - "Fast Fall Delay: How many frames the CPU should delay their fastfall" - ); - add_bitflag_submenu!( - overall_menu, - "Falling Aerials", - falling_aerials, - BoolFlag, - "Falling Aerials: Should aerials be performed when rising or when falling" - ); - add_bitflag_submenu!( - overall_menu, - "Full Hop", - full_hop, - BoolFlag, - "Full Hop: Should the CPU perform a full hop or a short hop" - ); - - add_bitflag_submenu!( - overall_menu, - "Shield Tilt", - shield_tilt, - Direction, - "Shield Tilt: Direction to tilt the shield" - ); - add_bitflag_submenu!( - overall_menu, - "DI Direction", - di_state, - Direction, - "DI Direction: Direction to angle the directional influence during hitlag" - ); - add_bitflag_submenu!( - overall_menu, - "SDI Direction", - sdi_state, - Direction, - "SDI Direction: Direction to angle the smash directional influence during hitlag" - ); - add_bitflag_submenu!( - overall_menu, - "Airdodge Direction", - air_dodge_dir, - Direction, - "Airdodge Direction: Direction to angle airdodges" - ); - - add_single_option_submenu!( - overall_menu, - "SDI Strength", - sdi_strength, - SdiStrength, - "SDI Strength: Relative strength of the smash directional influence inputs" - ); - add_single_option_submenu!( - overall_menu, - "Shield Toggles", - shield_state, - Shield, - "Shield Toggles: CPU Shield Behavior" - ); - add_single_option_submenu!( - overall_menu, - "Mirroring", - save_state_mirroring, - SaveStateMirroring, - "Mirroring: Flips save states in the left-right direction across the stage center" - ); - add_bitflag_submenu!( - overall_menu, - "Throw Options", - throw_state, - ThrowOption, - "Throw Options: Throw to be performed when a grab is landed" - ); - add_bitflag_submenu!( - overall_menu, - "Throw Delay", - throw_delay, - MedDelay, - "Throw Delay: How many frames to delay the throw option" - ); - add_bitflag_submenu!( - overall_menu, - "Pummel Delay", - pummel_delay, - MedDelay, - "Pummel Delay: How many frames after a grab to wait before starting to pummel" - ); - add_bitflag_submenu!( - overall_menu, - "Buff Options", - buff_state, - BuffOption, - "Buff Options: Buff(s) to be applied to respective character when loading save states" - ); - - // Slider menus - overall_menu.add_sub_menu( - "Input Delay", - "input_delay", - // unnecessary for slider? - "toggle", - 0, - [ - ("0", 0), - ("1", 1), - ("2", 2), - ("3", 3), - ("4", 4), - ("5", 5), - ("6", 6), - ("7", 7), - ("8", 8), - ("9", 9), - ("10", 10), - ] - .to_vec(), - [].to_vec(), //(0, 10, MENU.input_delay as usize) - DEFAULT_MENU.input_delay as usize, - stringify!("Input Delay: Frames to delay player inputs by"), - ); - - add_onoff_submenu!( - overall_menu, - "Save States", - save_state_enable, - "Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt." - ); - add_onoff_submenu!( - overall_menu, - "Save Damage", - save_damage, - "Save Damage: Should save states retain player/CPU damage" - ); - add_onoff_submenu!( - overall_menu, - "Hitbox Visualization", - hitbox_vis, - "Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects" - ); - add_onoff_submenu!( - overall_menu, - "Stage Hazards", - stage_hazards, - "Stage Hazards: Should stage hazards be present" - ); - add_onoff_submenu!( - overall_menu, - "Frame Advantage", - frame_advantage, - "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable"); - add_onoff_submenu!( - overall_menu, - "Mash In Neutral", - mash_in_neutral, - "Mash In Neutral: Should Mash options be performed repeatedly or only when the CPU is hit" - ); - add_onoff_submenu!( - overall_menu, - "Quick Menu", - quick_menu, - "Quick Menu: Whether to use Quick Menu or Web Menu" - ); - - overall_menu +impl<'a> Tab<'a> { + pub fn add_submenu_with_toggles<T: ToggleTrait>( + &mut self, + submenu_title: &'a str, + submenu_id: &'a str, + help_text: &'a str, + is_single_option: bool, + ) { + self.tab_submenus.push( + SubMenu::new_with_toggles::<T>( + submenu_title, + submenu_id, + help_text, + is_single_option, + ) + ); + } } -pub fn get_menu_from_url(mut menu: TrainingModpackMenu, s: &str) -> TrainingModpackMenu { +#[derive(Content)] +pub struct UiMenu<'a> { + pub tabs: Vec<Tab<'a>>, +} + +pub fn get_menu_from_url(mut menu: TrainingModpackMenu, s: &str, defaults: bool) -> TrainingModpackMenu { let base_url_len = "http://localhost/?".len(); let total_len = s.len(); @@ -1594,21 +1216,283 @@ pub fn get_menu_from_url(mut menu: TrainingModpackMenu, s: &str) -> TrainingModp for toggle_values in ss.split('&') { let toggle_value_split = toggle_values.split('=').collect::<Vec<&str>>(); - let toggle = toggle_value_split[0]; - if toggle.is_empty() { - continue; - } - - let toggle_vals = toggle_value_split[1]; - - let bitwise_or = <u32 as BitOr<u32>>::bitor; - let bits = toggle_vals - .split(',') - .filter(|val| !val.is_empty()) - .map(|val| val.parse().unwrap()) - .fold(0, bitwise_or); + let mut toggle = toggle_value_split[0]; + if toggle.is_empty() | ( + // Default menu settings begin with the prefix "__" + // So if skip toggles without the prefix if defaults is true + // And skip toggles with the prefix if defaults is false + defaults ^ toggle.starts_with("__") + ) { continue } + toggle = toggle.strip_prefix("__").unwrap_or(toggle); + let bits: u32 = toggle_value_split[1].parse().unwrap_or(0); menu.set(toggle, bits); } menu -} \ No newline at end of file +} + + + +pub unsafe fn get_menu() -> UiMenu<'static> { + let mut overall_menu = UiMenu { + tabs: Vec::new(), + }; + + let mut mash_tab = Tab { + tab_id: "mash", + tab_title: "Mash Settings", + tab_submenus: Vec::new(), + }; + mash_tab.add_submenu_with_toggles::<Action>( + "Mash Toggles", + "mash_state", + "Mash Toggles: Actions to be performed as soon as possible", + false, + ); + mash_tab.add_submenu_with_toggles::<Action>( + "Followup Toggles", + "follow_up", + "Followup Toggles: Actions to be performed after the Mash option", + false, + ); + mash_tab.add_submenu_with_toggles::<AttackAngle>( + "Attack Angle", + "attack_angle", + "Attack Angle: For attacks that can be angled, such as some forward tilts", + false, + ); + mash_tab.add_submenu_with_toggles::<ThrowOption>( + "Throw Options", + "throw_state", + "Throw Options: Throw to be performed when a grab is landed", + false, + ); + mash_tab.add_submenu_with_toggles::<MedDelay>( + "Throw Delay", + "throw_delay", + "Throw Delay: How many frames to delay the throw option", + false, + ); + mash_tab.add_submenu_with_toggles::<MedDelay>( + "Pummel Delay", + "pummel_delay", + "Pummel Delay: How many frames after a grab to wait before starting to pummel", + false, + ); + mash_tab.add_submenu_with_toggles::<BoolFlag>( + "Falling Aerials", + "falling_aerials", + "Falling Aerials: Should aerials be performed when rising or when falling", + false, // TODO: Should this be a single option submenu? + ); + mash_tab.add_submenu_with_toggles::<BoolFlag>( + "Full Hop", + "full_hop", + "Full Hop: Should the CPU perform a full hop or a short hop", + false, + ); + mash_tab.add_submenu_with_toggles::<Delay>( + "Aerial Delay", + "aerial_delay", + "Aerial Delay: How long to delay a Mash aerial attack", + false, + ); + mash_tab.add_submenu_with_toggles::<BoolFlag>( + "Fast Fall", + "fast_fall", + "Fast Fall: Should the CPU fastfall during a jump", + false, + ); + mash_tab.add_submenu_with_toggles::<Delay>( + "Fast Fall Delay", + "fast_fall_delay", + "Fast Fall Delay: How many frames the CPU should delay their fastfall", + false, + ); + mash_tab.add_submenu_with_toggles::<Delay>( + "OoS Offset", + "oos_offset", + "OoS Offset: How many times the CPU shield can be hit before performing a Mash option", + false, + ); + mash_tab.add_submenu_with_toggles::<Delay>( + "Reaction Time", + "reaction_time", + "Reaction Time: How many frames to delay before performing a mash option", + false, + ); + mash_tab.add_submenu_with_toggles::<OnOff>( + "Mash in Neutral", + "mash_in_neutral", + "Mash In Neutral: Should Mash options be performed repeatedly or only when the CPU is hit", + true, + ); + overall_menu.tabs.push(mash_tab); + + + let mut defensive_tab = Tab { + tab_id: "defensive", + tab_title: "Defensive Settings", + tab_submenus: Vec::new(), + }; + defensive_tab.add_submenu_with_toggles::<Direction>( + "Airdodge Direction", + "air_dodge_dir", + "Airdodge Direction: Direction to angle airdodges", + false, + ); + defensive_tab.add_submenu_with_toggles::<Direction>( + "DI Direction", + "di_state", + "DI Direction: Direction to angle the directional influence during hitlag", + false, + ); + defensive_tab.add_submenu_with_toggles::<Direction>( + "SDI Direction", + "sdi_state", + "SDI Direction: Direction to angle the smash directional influence during hitlag", + false, + ); + defensive_tab.add_submenu_with_toggles::<SdiStrength>( + "SDI Strength", + "sdi_strength", + "SDI Strength: Relative strength of the smash directional influence inputs", + true, + ); + defensive_tab.add_submenu_with_toggles::<LedgeOption>( + "Ledge Options", + "ledge_state", + "Ledge Options: Actions to be taken when on the ledge", + false, + ); + defensive_tab.add_submenu_with_toggles::<LongDelay>( + "Ledge Delay", + "ledge_delay", + "Ledge Delay: How many frames to delay the ledge option", + false, + ); + defensive_tab.add_submenu_with_toggles::<TechFlags>( + "Tech Options", + "tech_state", + "Tech Options: Actions to take when slammed into a hard surface", + false, + ); + defensive_tab.add_submenu_with_toggles::<MissTechFlags>( + "Mistech Options", + "miss_tech_state", + "Mistech Options: Actions to take after missing a tech", + false, + ); + defensive_tab.add_submenu_with_toggles::<Shield>( + "Shield Toggles", + "shield_state", + "Shield Toggles: CPU Shield Behavior", + true, + ); + defensive_tab.add_submenu_with_toggles::<Direction>( + "Shield Tilt", + "shield_tilt", + "Shield Tilt: Direction to tilt the shield", + false, // TODO: Should this be true? + ); + defensive_tab.add_submenu_with_toggles::<Defensive>( + "Defensive Toggles", + "defensive_state", + "Defensive Options: Actions to take after a ledge option, tech option, or mistech option", + false, + ); + defensive_tab.add_submenu_with_toggles::<BuffOption>( + "Buff Options", + "buff_state", + "Buff Options: Buff(s) to be applied to respective character when loading save states", + false, + ); + overall_menu.tabs.push(defensive_tab); + + let mut misc_tab = Tab { + tab_id: "misc", + tab_title: "Misc Settings", + tab_submenus: Vec::new(), + }; + misc_tab.add_submenu_with_toggles::<SaveStateMirroring>( + "Mirroring", + "save_state_mirroring", + "Mirroring: Flips save states in the left-right direction across the stage center", + true, + ); + misc_tab.add_submenu_with_toggles::<OnOff>( + "Save Damage", + "save_damage", + "Save Damage: Should save states retain player/CPU damage", + true, + ); + misc_tab.add_submenu_with_toggles::<OnOff>( + "Enable Save States", + "save_state_enable", + "Save States: Enable save states! Save a state with Grab+Down Taunt, load it with Grab+Up Taunt.", + true, + ); + misc_tab.add_submenu_with_toggles::<OnOff>( + "Frame Advantage", + "frame_advantage", + "Frame Advantage: Display the time difference between when the player is actionable and the CPU is actionable", + true, + ); + misc_tab.add_submenu_with_toggles::<OnOff>( + "Hitbox Visualization", + "hitbox_vis", + "Hitbox Visualization: Should hitboxes be displayed, hiding other visual effects", + true, + ); + misc_tab.add_submenu_with_toggles::<Delay>( + "Input Delay", + "input_delay", + "Input Delay: Frames to delay player inputs by", + true, + ); + misc_tab.add_submenu_with_toggles::<OnOff>( + "Stage Hazards", + "stage_hazards", + "Stage Hazards: Should stage hazards be present", + true + ); + misc_tab.add_submenu_with_toggles::<OnOff>( + "Quick Menu", + "quick_menu", + "Quick Menu: Should use quick or web menu", + true + ); + overall_menu.tabs.push(misc_tab); + + let non_ui_menu = MENU; + let url_params = non_ui_menu.to_url_params(false); + let toggle_values_all = url_params.split("&"); + let mut sub_menu_id_to_vals : HashMap<&str, Vec<u32>> = HashMap::new(); + for toggle_values in toggle_values_all { + let toggle_value_split = toggle_values.split('=').collect::<Vec<&str>>(); + let mut sub_menu_id = toggle_value_split[0]; + if sub_menu_id.is_empty() { continue } + sub_menu_id = sub_menu_id.strip_prefix("__").unwrap_or(sub_menu_id); + + let bits: u32 = toggle_value_split[1].parse().unwrap_or(0); + if sub_menu_id_to_vals.contains_key(sub_menu_id) { + sub_menu_id_to_vals.get_mut(sub_menu_id).unwrap().push(bits); + } else { + sub_menu_id_to_vals.insert(sub_menu_id, vec![bits]); + } + } + overall_menu.tabs.iter_mut() + .for_each(|tab| { + tab.tab_submenus.iter_mut().for_each(|sub_menu| { + let sub_menu_id = sub_menu.submenu_id; + sub_menu.toggles.iter_mut().for_each(|toggle| { + if sub_menu_id_to_vals.contains_key(sub_menu_id) && + sub_menu_id_to_vals[sub_menu_id].contains(&(toggle.toggle_value as u32)) { + toggle.checked = true + } + }) + }) + }); + + overall_menu +} diff --git a/training_mod_tui/src/lib.rs b/training_mod_tui/src/lib.rs index 2ac8da3..c19f0ff 100644 --- a/training_mod_tui/src/lib.rs +++ b/training_mod_tui/src/lib.rs @@ -1,4 +1,4 @@ -use training_mod_consts::{OnOffSelector, Slider, SubMenu, SubMenuType, Toggle}; +use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu}; use tui::{ backend::{Backend}, layout::{Constraint, Corner, Direction, Layout}, @@ -22,81 +22,26 @@ pub struct App<'a> { pub tabs: StatefulList<&'a str>, pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>, pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>, - pub selected_sub_menu_onoff_selectors: MultiStatefulList<OnOffSelector<'a>>, pub selected_sub_menu_sliders: MultiStatefulList<Slider>, pub outer_list: bool } impl<'a> App<'a> { - pub fn new(menu: training_mod_consts::Menu<'a>) -> App<'a> { - let tab_specifiers = vec![ - ("Mash Settings", vec![ - "Mash Toggles", - "Followup Toggles", - "Attack Angle", - "Ledge Options", - "Ledge Delay", - "Tech Options", - "Miss Tech Options", - "Defensive Options", - "Aerial Delay", - "OoS Offset", - "Reaction Time", - ]), - ("Defensive Settings", vec![ - "Fast Fall", - "Fast Fall Delay", - "Falling Aerials", - "Full Hop", - "Shield Tilt", - "DI Direction", - "SDI Direction", - "Airdodge Direction", - "SDI Strength", - "Shield Toggles", - "Mirroring", - "Throw Options", - "Throw Delay", - "Pummel Delay", - "Buff Options", - ]), - ("Other Settings", vec![ - "Input Delay", - "Save States", - "Save Damage", - "Hitbox Visualization", - "Stage Hazards", - "Frame Advantage", - "Mash In Neutral", - "Quick Menu" - ]) - ]; - let mut tabs: std::collections::HashMap<&str, Vec<SubMenu>> = std::collections::HashMap::new(); - tabs.insert("Mash Settings", vec![]); - tabs.insert("Defensive Settings", vec![]); - tabs.insert("Other Settings", vec![]); - - for sub_menu in menu.sub_menus.iter() { - for tab_spec in tab_specifiers.iter() { - if tab_spec.1.contains(&sub_menu.title) { - tabs.get_mut(tab_spec.0).unwrap().push(sub_menu.clone()); - } - } - }; + pub fn new(menu: UiMenu<'a>) -> App<'a> { let num_lists = 3; let mut menu_items_stateful = HashMap::new(); - tabs.keys().for_each(|k| { + menu.tabs.iter().for_each(|tab| { menu_items_stateful.insert( - k.clone(), - MultiStatefulList::with_items(tabs.get(k).unwrap().clone(), num_lists) + tab.tab_title, + MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists) ); }); + let mut app = App { - tabs: StatefulList::with_items(tab_specifiers.iter().map(|(tab_title, _)| *tab_title).collect()), + tabs: StatefulList::with_items(menu.tabs.iter().map(|tab| tab.tab_title).collect()), menu_items: menu_items_stateful, selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0), - selected_sub_menu_onoff_selectors: MultiStatefulList::with_items(vec![], 0), selected_sub_menu_sliders: MultiStatefulList::with_items(vec![], 0), outer_list: true }; @@ -109,8 +54,7 @@ impl<'a> App<'a> { let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap(); let toggles = selected_sub_menu.toggles.clone(); - let sliders = selected_sub_menu.sliders.clone(); - let onoffs = selected_sub_menu.onoffselector.clone(); + // let sliders = selected_sub_menu.sliders.clone(); match SubMenuType::from_str(self.sub_menu_selected()._type) { SubMenuType::TOGGLE => { self.selected_sub_menu_toggles = MultiStatefulList::with_items( @@ -118,14 +62,9 @@ impl<'a> App<'a> { if selected_sub_menu.toggles.len() >= 3 { 3 } else { selected_sub_menu.toggles.len()} ) }, SubMenuType::SLIDER => { - self.selected_sub_menu_sliders = MultiStatefulList::with_items( - sliders, - if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} ) - }, - SubMenuType::ONOFF => { - self.selected_sub_menu_onoff_selectors = MultiStatefulList::with_items( - onoffs, - if selected_sub_menu.onoffselector.len() >= 3 { 3 } else { selected_sub_menu.onoffselector.len()} ) + // self.selected_sub_menu_sliders = MultiStatefulList::with_items( + // sliders, + // if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} ) }, }; } @@ -136,14 +75,13 @@ impl<'a> App<'a> { fn sub_menu_selected(&self) -> &SubMenu { let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state); - &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap() + self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap() } pub fn sub_menu_next(&mut self) { match SubMenuType::from_str(self.sub_menu_selected()._type) { SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.next(), - SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next(), } } @@ -151,7 +89,6 @@ impl<'a> App<'a> { match SubMenuType::from_str(self.sub_menu_selected()._type) { SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.next_list(), - SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next_list(), } } @@ -159,7 +96,6 @@ impl<'a> App<'a> { match SubMenuType::from_str(self.sub_menu_selected()._type) { SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous(), - SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous(), } } @@ -167,30 +103,22 @@ impl<'a> App<'a> { match SubMenuType::from_str(self.sub_menu_selected()._type) { SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous_list(), - SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous_list(), } } - pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(&str, &str)>, ListState)>) { - (self.sub_menu_selected().title, self.sub_menu_selected().help_text, + pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(bool, &str)>, ListState)>) { + (self.sub_menu_selected().submenu_title, self.sub_menu_selected().help_text, match SubMenuType::from_str(self.sub_menu_selected()._type) { SubMenuType::TOGGLE => { self.selected_sub_menu_toggles.lists.iter().map(|toggle_list| { (toggle_list.items.iter().map( - |toggle| (toggle.checked, toggle.title) + |toggle| (toggle.checked, toggle.toggle_title) ).collect(), toggle_list.state.clone()) }).collect() }, SubMenuType::SLIDER => { vec![(vec![], ListState::default())] }, - SubMenuType::ONOFF => { - self.selected_sub_menu_onoff_selectors.lists.iter().map(|onoff_list| { - (onoff_list.items.iter().map( - |onoff| (onoff.checked, onoff.title) - ).collect(), onoff_list.state.clone()) - }).collect() - }, }) } @@ -208,7 +136,7 @@ impl<'a> App<'a> { .items.get_mut(list_idx).unwrap(); match SubMenuType::from_str(selected_sub_menu._type) { SubMenuType::TOGGLE => { - let is_single_option = selected_sub_menu.is_single_option.is_some(); + let is_single_option = selected_sub_menu.is_single_option; let state = self.selected_sub_menu_toggles.state; self.selected_sub_menu_toggles.lists.iter_mut() .map(|list| (list.state.selected(), &mut list.items)) @@ -216,42 +144,31 @@ impl<'a> App<'a> { .enumerate() .for_each(|(i, o)| if state.is_some() && i == state.unwrap() { - if o.checked != "is-appear" { - o.checked = "is-appear"; + if !o.checked { + o.checked = true; } else { - o.checked = "is-hidden"; + o.checked = false; } } else if is_single_option { - o.checked = "is-hidden"; + o.checked = false; } )); selected_sub_menu.toggles.iter_mut() .enumerate() .for_each(|(i, o)| { if i == state { - if o.checked != "is-appear" { - o.checked = "is-appear"; + if !o.checked { + o.checked = true; } else { - o.checked = "is-hidden"; + o.checked = false; } } else if is_single_option { - o.checked = "is-hidden"; + o.checked = false; } }); }, - SubMenuType::ONOFF => { - let onoff = self.selected_sub_menu_onoff_selectors.selected_list_item(); - if onoff.checked != "is-appear" { - onoff.checked = "is-appear"; - } else { - onoff.checked = "is-hidden"; - } - selected_sub_menu.onoffselector.iter_mut() - .filter(|o| o.title == onoff.title) - .for_each(|o| o.checked = onoff.checked); - }, SubMenuType::SLIDER => { - // self.selected_sub_menu_sliders.selected_list_item().checked = "is-appear"; + // self.selected_sub_menu_sliders.selected_list_item().checked = true; } } } @@ -352,17 +269,16 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String { if app.outer_list { let tab_selected = app.tab_selected(); let mut item_help = None; - for list_section in 0..app.menu_items.get(tab_selected).unwrap().lists.len() { - let stateful_list = &app.menu_items.get(tab_selected).unwrap().lists[list_section]; + for (list_section, stateful_list) in app.menu_items.get(tab_selected).unwrap().lists.iter().enumerate() { let items: Vec<ListItem> = stateful_list .items .iter() .map(|i| { let lines = vec![Spans::from( if stateful_list.state.selected().is_some() { - i.title.to_owned() + i.submenu_title.to_owned() } else { - " ".to_owned() + i.title + " ".to_owned() + i.submenu_title })]; ListItem::new(lines).style(Style::default().fg(Color::White)) }) @@ -389,7 +305,7 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String { // TODO: Add Save Defaults let help_paragraph = Paragraph::new( - item_help.unwrap_or("").replace("\"", "") + + item_help.unwrap_or("").replace('\"', "") + "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab" ).style(Style::default().fg(Color::Cyan)); f.render_widget(help_paragraph, vertical_chunks[2]); @@ -401,7 +317,7 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String { let values_items: Vec<ListItem> = sub_menu_str.iter().map(|s| { ListItem::new( vec![ - Spans::from((if s.0 == "is-appear" { "X " } else { " " }).to_owned() + s.1) + Spans::from((if s.0 { "X " } else { " " }).to_owned() + s.1) ] ) }).collect(); @@ -419,7 +335,7 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String { } let help_paragraph = Paragraph::new( - help_text.replace("\"", "") + + help_text.replace('\"', "") + "\nA: Select toggle | B: Exit submenu" ).style(Style::default().fg(Color::Cyan)); f.render_widget(help_paragraph, vertical_chunks[2]); @@ -433,22 +349,17 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String { for key in app.menu_items.keys() { for list in &app.menu_items.get(key).unwrap().lists { for sub_menu in &list.items { - let mut val = String::new(); - sub_menu.toggles.iter() - .filter(|t| t.checked == "is-appear") - .for_each(|t| val.push_str(format!("{},", t.value).as_str())); + let val : usize = sub_menu.toggles.iter() + .filter(|t| t.checked) + .map(|t| t.toggle_value) + .sum(); - sub_menu.onoffselector.iter() - .for_each(|o| { - val.push_str( - format!("{}", if o.checked == "is-appear" { 1 } else { 0 }).as_str()) - }); - settings.insert(sub_menu.id, val); + settings.insert(sub_menu.submenu_id, val); } } } - url.push_str("?"); + url.push('?'); settings.iter() .for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str())); url diff --git a/training_mod_tui/src/list.rs b/training_mod_tui/src/list.rs index a8cd63b..2e0d337 100644 --- a/training_mod_tui/src/list.rs +++ b/training_mod_tui/src/list.rs @@ -49,14 +49,14 @@ impl<T: Clone> MultiStatefulList<T> { state.select(Some(0)); } StatefulList { - state: state, + state, items: items[list_section_min_idx..list_section_max_idx].to_vec(), } }).collect(); let total_len = items.len(); MultiStatefulList { - lists: lists, - total_len: total_len, + lists, + total_len, state: 0 } } @@ -68,12 +68,11 @@ impl<T: Clone> MultiStatefulList<T> { if list_section != next_list_section { self.lists[list_section].unselect(); } - let state; - if self.state + 1 >= self.total_len { - state = (0, 0); + let state= if self.state + 1 >= self.total_len { + (0, 0) } else { - state = (next_list_section, next_list_idx); - } + (next_list_section, next_list_idx) + }; self.lists[state.0].state.select(Some(state.1)); self.state = self.list_idx_to_idx(state); @@ -84,13 +83,12 @@ impl<T: Clone> MultiStatefulList<T> { let (last_list_section, last_list_idx) = (self.lists.len() - 1, self.lists[self.lists.len() - 1].items.len() - 1); self.lists[list_section].unselect(); - let state; - if self.state == 0 { - state = (last_list_section, last_list_idx); + let state= if self.state == 0 { + (last_list_section, last_list_idx) } else { let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1); - state = (prev_list_section, prev_list_idx); - } + (prev_list_section, prev_list_idx) + }; self.lists[state.0].state.select(Some(state.1)); self.state = self.list_idx_to_idx(state); @@ -99,12 +97,11 @@ impl<T: Clone> MultiStatefulList<T> { pub fn next_list(&mut self) { let (list_section, list_idx) = self.idx_to_list_idx(self.state); let next_list_section = (list_section + 1) % self.lists.len(); - let next_list_idx; - if list_idx > self.lists[next_list_section].items.len() - 1 { - next_list_idx = self.lists[next_list_section].items.len() - 1; + let next_list_idx = if list_idx > self.lists[next_list_section].items.len() - 1 { + self.lists[next_list_section].items.len() - 1 } else { - next_list_idx = list_idx; - } + list_idx + }; if list_section != next_list_section { self.lists[list_section].unselect(); @@ -117,19 +114,17 @@ impl<T: Clone> MultiStatefulList<T> { pub fn previous_list(&mut self) { let (list_section, list_idx) = self.idx_to_list_idx(self.state); - let prev_list_section; - if list_section == 0 { - prev_list_section = self.lists.len() - 1; + let prev_list_section = if list_section == 0 { + self.lists.len() - 1 } else { - prev_list_section = list_section - 1; - } + list_section - 1 + }; - let prev_list_idx; - if list_idx > self.lists[prev_list_section].items.len() - 1 { - prev_list_idx = self.lists[prev_list_section].items.len() - 1; + let prev_list_idx= if list_idx > self.lists[prev_list_section].items.len() - 1 { + self.lists[prev_list_section].items.len() - 1 } else { - prev_list_idx = list_idx; - } + list_idx + }; if list_section != prev_list_section { self.lists[list_section].unselect(); @@ -152,7 +147,7 @@ impl<T> StatefulList<T> { // Enforce state as first of list state.select(Some(0)); StatefulList { - state: state, + state, items, } } diff --git a/training_mod_tui/src/main.rs b/training_mod_tui/src/main.rs index d8353e2..9e4424e 100644 --- a/training_mod_tui/src/main.rs +++ b/training_mod_tui/src/main.rs @@ -32,10 +32,10 @@ fn main() -> Result<(), Box<dyn Error>> { let mut url = String::new(); let frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?; - for (i, cell) in frame_res.buffer.content().into_iter().enumerate() { + for (i, cell) in frame_res.buffer.content().iter().enumerate() { print!("{}", cell.symbol); if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 { - print!("\n"); + println!(); } } println!();